unit tests: add a project plan document
In our current testing environment, we spend a significant amount of effort crafting end-to-end tests for error conditions that could easily be captured by unit tests (or we simply forgo some hard-to-setup and rare error conditions). Describe what we hope to accomplish by implementing unit tests, and explain some open questions and milestones. Discuss desired features for test frameworks/harnesses, and provide a comparison of several different frameworks. Finally, document our rationale for implementing a custom framework. Co-authored-by: Calvin Wan <calvinwan@google.com> Signed-off-by: Calvin Wan <calvinwan@google.com> Signed-off-by: Josh Steadmon <steadmon@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Josh Steadmon committed
Nov 9, 2023 at 10:50 UTC
581790eeee560c791eeaa9be3dd9173d3686acc6
2 files changed
+241
Documentation/Makefile
+1
@@ -122,6 +122,7 @@ TECH_DOCS += technical/scalar
122
TECH_DOCS += technical/send-pack-pipeline
123
TECH_DOCS += technical/shallow
124
TECH_DOCS += technical/trivial-merge
125
+TECH_DOCS += technical/unit-tests
126
SP_ARTICLES += $(TECH_DOCS)
127
SP_ARTICLES += technical/api-index
128
Documentation/technical/unit-tests.txt
new
+240
@@ -0,0 +1,240 @@
1
+= Unit Testing
2
+
3
+In our current testing environment, we spend a significant amount of effort
4
+crafting end-to-end tests for error conditions that could easily be captured by
5
+unit tests (or we simply forgo some hard-to-setup and rare error conditions).
6
+Unit tests additionally provide stability to the codebase and can simplify
7
+debugging through isolation. Writing unit tests in pure C, rather than with our
8
+current shell/test-tool helper setup, simplifies test setup, simplifies passing
9
+data around (no shell-isms required), and reduces testing runtime by not
10
+spawning a separate process for every test invocation.
11
+
12
+We believe that a large body of unit tests, living alongside the existing test
13
+suite, will improve code quality for the Git project.
14
+
15
+== Definitions
16
+
17
+For the purposes of this document, we'll use *test framework* to refer to
18
+projects that support writing test cases and running tests within the context
19
+of a single executable. *Test harness* will refer to projects that manage
20
+running multiple executables (each of which may contain multiple test cases) and
21
+aggregating their results.
22
+
23
+In reality, these terms are not strictly defined, and many of the projects
24
+discussed below contain features from both categories.
25
+
26
+For now, we will evaluate projects solely on their framework features. Since we
27
+are relying on having TAP output (see below), we can assume that any framework
28
+can be made to work with a harness that we can choose later.
29
+
30
+
31
+== Summary
32
+
33
+We believe the best way forward is to implement a custom TAP framework for the
34
+Git project. We use a version of the framework originally proposed in
35
+https://lore.kernel.org/git/c902a166-98ce-afba-93f2-ea6027557176@gmail.com/[1].
36
+
37
+See the <<framework-selection,Framework Selection>> section below for the
38
+rationale behind this decision.
39
+
40
+
41
+== Choosing a test harness
42
+
43
+During upstream discussion, it was occasionally noted that `prove` provides many
44
+convenient features, such as scheduling slower tests first, or re-running
45
+previously failed tests.
46
+
47
+While we already support the use of `prove` as a test harness for the shell
48
+tests, it is not strictly required. The t/Makefile allows running shell tests
49
+directly (though with interleaved output if parallelism is enabled). Git
50
+developers who wish to use `prove` as a more advanced harness can do so by
51
+setting DEFAULT_TEST_TARGET=prove in their config.mak.
52
+
53
+We will follow a similar approach for unit tests: by default the test
54
+executables will be run directly from the t/Makefile, but `prove` can be
55
+configured with DEFAULT_UNIT_TEST_TARGET=prove.
56
+
57
+
58
+[[framework-selection]]
59
+== Framework selection
60
+
61
+There are a variety of features we can use to rank the candidate frameworks, and
62
+those features have different priorities:
63
+
64
+* Critical features: we probably won't consider a framework without these
65
+** Can we legally / easily use the project?
66
+*** <<license,License>>
67
+*** <<vendorable-or-ubiquitous,Vendorable or ubiquitous>>
68
+*** <<maintainable-extensible,Maintainable / extensible>>
69
+*** <<major-platform-support,Major platform support>>
70
+** Does the project support our bare-minimum needs?
71
+*** <<tap-support,TAP support>>
72
+*** <<diagnostic-output,Diagnostic output>>
73
+*** <<runtime-skippable-tests,Runtime-skippable tests>>
74
+* Nice-to-have features:
75
+** <<parallel-execution,Parallel execution>>
76
+** <<mock-support,Mock support>>
77
+** <<signal-error-handling,Signal & error-handling>>
78
+* Tie-breaker stats
79
+** <<project-kloc,Project KLOC>>
80
+** <<adoption,Adoption>>
81
+
82
+[[license]]
83
+=== License
84
+
85
+We must be able to legally use the framework in connection with Git. As Git is
86
+licensed only under GPLv2, we must eliminate any LGPLv3, GPLv3, or Apache 2.0
87
+projects.
88
+
89
+[[vendorable-or-ubiquitous]]
90
+=== Vendorable or ubiquitous
91
+
92
+We want to avoid forcing Git developers to install new tools just to run unit
93
+tests. Any prospective frameworks and harnesses must either be vendorable
94
+(meaning, we can copy their source directly into Git's repository), or so
95
+ubiquitous that it is reasonable to expect that most developers will have the
96
+tools installed already.
97
+
98
+[[maintainable-extensible]]
99
+=== Maintainable / extensible
100
+
101
+It is unlikely that any pre-existing project perfectly fits our needs, so any
102
+project we select will need to be actively maintained and open to accepting
103
+changes. Alternatively, assuming we are vendoring the source into our repo, it
104
+must be simple enough that Git developers can feel comfortable making changes as
105
+needed to our version.
106
+
107
+In the comparison table below, "True" means that the framework seems to have
108
+active developers, that it is simple enough that Git developers can make changes
109
+to it, and that the project seems open to accepting external contributions (or
110
+that it is vendorable). "Partial" means that at least one of the above
111
+conditions holds.
112
+
113
+[[major-platform-support]]
114
+=== Major platform support
115
+
116
+At a bare minimum, unit-testing must work on Linux, MacOS, and Windows.
117
+
118
+In the comparison table below, "True" means that it works on all three major
119
+platforms with no issues. "Partial" means that there may be annoyances on one or
120
+more platforms, but it is still usable in principle.
121
+
122
+[[tap-support]]
123
+=== TAP support
124
+
125
+The https://testanything.org/[Test Anything Protocol] is a text-based interface
126
+that allows tests to communicate with a test harness. It is already used by
127
+Git's integration test suite. Supporting TAP output is a mandatory feature for
128
+any prospective test framework.
129
+
130
+In the comparison table below, "True" means this is natively supported.
131
+"Partial" means TAP output must be generated by post-processing the native
132
+output.
133
+
134
+Frameworks that do not have at least Partial support will not be evaluated
135
+further.
136
+
137
+[[diagnostic-output]]
138
+=== Diagnostic output
139
+
140
+When a test case fails, the framework must generate enough diagnostic output to
141
+help developers find the appropriate test case in source code in order to debug
142
+the failure.
143
+
144
+[[runtime-skippable-tests]]
145
+=== Runtime-skippable tests
146
+
147
+Test authors may wish to skip certain test cases based on runtime circumstances,
148
+so the framework should support this.
149
+
150
+[[parallel-execution]]
151
+=== Parallel execution
152
+
153
+Ideally, we will build up a significant collection of unit test cases, most
154
+likely split across multiple executables. It will be necessary to run these
155
+tests in parallel to enable fast develop-test-debug cycles.
156
+
157
+In the comparison table below, "True" means that individual test cases within a
158
+single test executable can be run in parallel. We assume that executable-level
159
+parallelism can be handled by the test harness.
160
+
161
+[[mock-support]]
162
+=== Mock support
163
+
164
+Unit test authors may wish to test code that interacts with objects that may be
165
+inconvenient to handle in a test (e.g. interacting with a network service).
166
+Mocking allows test authors to provide a fake implementation of these objects
167
+for more convenient tests.
168
+
169
+[[signal-error-handling]]
170
+=== Signal & error handling
171
+
172
+The test framework should fail gracefully when test cases are themselves buggy
173
+or when they are interrupted by signals during runtime.
174
+
175
+[[project-kloc]]
176
+=== Project KLOC
177
+
178
+The size of the project, in thousands of lines of code as measured by
179
+https://dwheeler.com/sloccount/[sloccount] (rounded up to the next multiple of
180
+1,000). As a tie-breaker, we probably prefer a project with fewer LOC.
181
+
182
+[[adoption]]
183
+=== Adoption
184
+
185
+As a tie-breaker, we prefer a more widely-used project. We use the number of
186
+GitHub / GitLab stars to estimate this.
187
+
188
+
189
+=== Comparison
190
+
191
+:true: [lime-background]#True#
192
+:false: [red-background]#False#
193
+:partial: [yellow-background]#Partial#
194
+
195
+:gpl: [lime-background]#GPL v2#
196
+:isc: [lime-background]#ISC#
197
+:mit: [lime-background]#MIT#
198
+:expat: [lime-background]#Expat#
199
+:lgpl: [lime-background]#LGPL v2.1#
200
+
201
+:custom-impl: https://lore.kernel.org/git/c902a166-98ce-afba-93f2-ea6027557176@gmail.com/[Custom Git impl.]
202
+:greatest: https://github.com/silentbicycle/greatest[Greatest]
203
+:criterion: https://github.com/Snaipe/Criterion[Criterion]
204
+:c-tap: https://github.com/rra/c-tap-harness/[C TAP]
205
+:check: https://libcheck.github.io/check/[Check]
206
+
207
+[format="csv",options="header",width="33%",subs="specialcharacters,attributes,quotes,macros"]
208
+|=====
209
+Framework,"<<license,License>>","<<vendorable-or-ubiquitous,Vendorable or ubiquitous>>","<<maintainable-extensible,Maintainable / extensible>>","<<major-platform-support,Major platform support>>","<<tap-support,TAP support>>","<<diagnostic-output,Diagnostic output>>","<<runtime--skippable-tests,Runtime- skippable tests>>","<<parallel-execution,Parallel execution>>","<<mock-support,Mock support>>","<<signal-error-handling,Signal & error handling>>","<<project-kloc,Project KLOC>>","<<adoption,Adoption>>"
210
+{custom-impl},{gpl},{true},{true},{true},{true},{true},{true},{false},{false},{false},1,0
211
+{greatest},{isc},{true},{partial},{true},{partial},{true},{true},{false},{false},{false},3,1400
212
+{criterion},{mit},{false},{partial},{true},{true},{true},{true},{true},{false},{true},19,1800
213
+{c-tap},{expat},{true},{partial},{partial},{true},{false},{true},{false},{false},{false},4,33
214
+{check},{lgpl},{false},{partial},{true},{true},{true},{false},{false},{false},{true},17,973
215
+|=====
216
+
217
+=== Additional framework candidates
218
+
219
+Several suggested frameworks have been eliminated from consideration:
220
+
221
+* Incompatible licenses:
222
+** https://github.com/zorgnax/libtap[libtap] (LGPL v3)
223
+** https://cmocka.org/[cmocka] (Apache 2.0)
224
+* Missing source: https://www.kindahl.net/mytap/doc/index.html[MyTap]
225
+* No TAP support:
226
+** https://nemequ.github.io/munit/[µnit]
227
+** https://github.com/google/cmockery[cmockery]
228
+** https://github.com/lpabon/cmockery2[cmockery2]
229
+** https://github.com/ThrowTheSwitch/Unity[Unity]
230
+** https://github.com/siu/minunit[minunit]
231
+** https://cunit.sourceforge.net/[CUnit]
232
+
233
+
234
+== Milestones
235
+
236
+* Add useful tests of library-like code
237
+* Integrate with
238
+ https://lore.kernel.org/git/20230502211454.1673000-1-calvinwan@google.com/[stdlib
239
+ work]
240
+* Run alongside regular `make test` target