Skip to content

Commit cba81fd

Browse files
committed
feat: add testname execution
1 parent 223c352 commit cba81fd

File tree

4 files changed

+206
-21
lines changed

4 files changed

+206
-21
lines changed

src/Builder/Builder.cc

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Result<void> Builder::build() {
6868
return Ok();
6969
}
7070

71-
Result<void> Builder::test() {
71+
Result<void> Builder::test(std::optional<std::string> testName) {
7272
Try(ensurePlanned());
7373

7474
const Manifest& mf = graphState->manifest();
@@ -109,6 +109,7 @@ Result<void> Builder::test() {
109109

110110
std::size_t numPassed = 0;
111111
std::size_t numFailed = 0;
112+
std::size_t numFilteredOut = 0;
112113
ExitStatus summaryStatus(EXIT_SUCCESS);
113114

114115
const auto labelFor = [](BuildGraph::TestKind kind) {
@@ -121,29 +122,35 @@ Result<void> Builder::test() {
121122
std::unreachable();
122123
};
123124

124-
for (const auto& target : targets) {
125-
const fs::path absoluteBinary = outDir / target.ninjaTarget;
126-
const std::string testBinPath =
127-
fs::relative(absoluteBinary, mf.path.parent_path()).string();
128-
Diag::info("Running", "{} test {} ({})", labelFor(target.kind),
129-
target.sourcePath, testBinPath);
130-
131-
const ExitStatus curExitStatus =
132-
Try(execCmd(Command(absoluteBinary.string())));
133-
if (curExitStatus.success()) {
134-
++numPassed;
125+
for (const auto& testTarget : targets) {
126+
if (!testName.has_value()
127+
|| testTarget.ninjaTarget.find(*testName) != std::string::npos) {
128+
129+
const fs::path absoluteBinary = outDir / testTarget.ninjaTarget;
130+
const std::string testBinPath =
131+
fs::relative(absoluteBinary, mf.path.parent_path()).string();
132+
Diag::info("Running", "{} test {} ({})", labelFor(testTarget.kind),
133+
testTarget.sourcePath, testBinPath);
134+
135+
const ExitStatus curExitStatus =
136+
Try(execCmd(Command(absoluteBinary.string())));
137+
if (curExitStatus.success()) {
138+
++numPassed;
139+
} else {
140+
++numFailed;
141+
summaryStatus = curExitStatus;
142+
}
135143
} else {
136-
++numFailed;
137-
summaryStatus = curExitStatus;
144+
++numFilteredOut;
138145
}
139146
}
140147

141148
const auto runEnd = std::chrono::steady_clock::now();
142149
const std::chrono::duration<double> runElapsed = runEnd - runStart;
143150

144151
const std::string summary =
145-
fmt::format("{} passed; {} failed; finished in {:.2f}s", numPassed,
146-
numFailed, runElapsed.count());
152+
fmt::format("{} passed; {} failed; {} filtered out; finished in {:.2f}s",
153+
numPassed, numFailed, numFilteredOut, runElapsed.count());
147154
if (!summaryStatus.success()) {
148155
return Err(anyhow::anyhow(summary));
149156
}

src/Builder/Builder.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Builder {
2626

2727
Result<void> schedule(const ScheduleOptions& options = {});
2828
Result<void> build();
29-
Result<void> test();
29+
Result<void> test(std::optional<std::string> testName);
3030
Result<void> run(const std::vector<std::string>& args);
3131

3232
const BuildGraph& graph() const;

src/Cmd/Test.cc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ const Subcmd TEST_CMD = //
2626
.setDesc("Run the tests of a local package")
2727
.addOpt(OPT_JOBS)
2828
.addOpt(Opt{ "--coverage" }.setDesc("Enable code coverage analysis"))
29+
.setArg(
30+
Arg{ "TESTNAME" }.setRequired(false).setDesc("Test name to launch"))
2931
.setMainFn(testMain);
3032

3133
static Result<void> testMain(const CliArgsView args) {
3234
bool enableCoverage = false;
35+
std::optional<std::string> testName;
3336

3437
for (auto itr = args.begin(); itr != args.end(); ++itr) {
3538
const std::string_view arg = *itr;
@@ -54,6 +57,8 @@ static Result<void> testMain(const CliArgsView args) {
5457
setParallelism(numThreads);
5558
} else if (arg == "--coverage") {
5659
enableCoverage = true;
60+
} else if (!testName) {
61+
testName = arg;
5762
} else {
5863
return TEST_CMD.noSuchArg(arg);
5964
}
@@ -63,7 +68,7 @@ static Result<void> testMain(const CliArgsView args) {
6368
Builder builder(manifest.path.parent_path(), BuildProfile::Test);
6469
Try(builder.schedule(ScheduleOptions{ .includeDevDeps = true,
6570
.enableCoverage = enableCoverage }));
66-
return builder.test();
71+
return builder.test(testName);
6772
}
6873

6974
} // namespace cabin

tests/test.cc

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <fmt/format.h>
66
#include <string>
77
#include <utility>
8+
#include <vector>
89

910
static std::size_t countFiles(const tests::fs::path& root,
1011
std::string_view extension) {
@@ -20,6 +21,42 @@ static std::size_t countFiles(const tests::fs::path& root,
2021
return count;
2122
}
2223

24+
struct TestInfo {
25+
std::string projectName;
26+
std::vector<std::string> testTargets;
27+
bool hasLib = false;
28+
29+
std::size_t numPassed = 0;
30+
std::size_t numFailed = 0;
31+
std::size_t numFiltered = 0;
32+
};
33+
34+
static std::string expectedTestSummary(const TestInfo& testInfo) {
35+
std::string summary = " Analyzing project dependencies...\n";
36+
if (testInfo.hasLib) {
37+
summary += fmt::format(" Compiling {}(lib) v0.1.0 (<PROJECT>)\n",
38+
testInfo.projectName);
39+
}
40+
summary += fmt::format(
41+
" Compiling {}(test) v0.1.0 (<PROJECT>)\n"
42+
" Finished `test` profile [unoptimized + debuginfo] target(s) in "
43+
"<DURATION>s\n",
44+
testInfo.projectName);
45+
46+
for (std::string_view testTarget : testInfo.testTargets) {
47+
summary += fmt::format(" Running unit test src/{0}.cc "
48+
"(cabin-out/test/unit/src/{0}.cc.test)\n",
49+
testTarget);
50+
}
51+
52+
summary += fmt::format(
53+
" Ok {} passed; {} failed; {} filtered out; finished in "
54+
"<DURATION>s\n",
55+
testInfo.numPassed, testInfo.numFailed, testInfo.numFiltered);
56+
57+
return summary;
58+
}
59+
2360
static std::string expectedTestSummary(std::string_view projectName,
2461
bool hasLib) {
2562
std::string summary = " Analyzing project dependencies...\n";
@@ -34,7 +71,8 @@ static std::string expectedTestSummary(std::string_view projectName,
3471
"<DURATION>s\n"
3572
" Running unit test src/main.cc "
3673
"(cabin-out/test/unit/src/main.cc.test)\n"
37-
" Ok 1 passed; 0 failed; finished in <DURATION>s\n";
74+
" Ok 1 passed; 0 failed; 0 filtered out; finished in "
75+
"<DURATION>s\n";
3876
return summary;
3977
}
4078

@@ -55,8 +93,8 @@ int main() {
5593
void test_addition() {
5694
int result = 2 + 2;
5795
if (result != 4) {
58-
std::cerr << "Test failed: 2 + 2 = " << result << ", expected 4" << std::endl;
59-
std::exit(1);
96+
std::cerr << "Test failed: 2 + 2 = " << result << ", expected 4" <<
97+
std::endl; std::exit(1);
6098
}
6199
std::cout << "test test addition ... ok" << std::endl;
62100
}
@@ -260,4 +298,139 @@ int main() {
260298
const auto outDir = project / "cabin-out" / "test" / "unit" / "lib";
261299
expect(tests::fs::is_regular_file(outDir / "lib_only.cc.test"));
262300
};
301+
302+
"cabin test testname filters single test"_test = [] {
303+
const tests::TempDir tmp;
304+
tests::runCabin({ "new", "testname_project" }, tmp.path).unwrap();
305+
const auto project = tmp.path / "testname_project";
306+
const auto projectPath = tests::fs::weakly_canonical(project).string();
307+
308+
tests::writeFile(project / "src/main.cc",
309+
R"(#include <iostream>
310+
311+
#ifdef CABIN_TEST
312+
void test_function() {
313+
std::cout << "main test function ... ok" << std::endl;
314+
}
315+
316+
int main() {
317+
test_function();
318+
return 0;
319+
}
320+
#else
321+
int main() {
322+
std::cout << "Hello, world!" << std::endl;
323+
return 0;
324+
}
325+
#endif
326+
)");
327+
328+
tests::writeFile(project / "src/Testname.cc",
329+
R"(#include <iostream>
330+
331+
#ifdef CABIN_TEST
332+
void test_function() {
333+
std::cout << "testname test function ... ok" << std::endl;
334+
}
335+
336+
int main() {
337+
test_function();
338+
return 0;
339+
}
340+
#endif
341+
)");
342+
343+
const auto result =
344+
tests::runCabin({ "test", "Testname" }, project).unwrap();
345+
expect(result.status.success());
346+
auto sanitizedOut = tests::sanitizeOutput(
347+
result.out, { { projectPath, "<PROJECT>" } }); // NOLINT
348+
expect(sanitizedOut == "testname test function ... ok\n");
349+
auto sanitizedErr = tests::sanitizeOutput(
350+
result.err, { { projectPath, "<PROJECT>" } }); // NOLINT
351+
352+
expect(sanitizedErr
353+
== expectedTestSummary(TestInfo{ .projectName = "testname_project",
354+
.testTargets = { "Testname" },
355+
.numPassed = 1,
356+
.numFailed = 0,
357+
.numFiltered = 1 }));
358+
};
359+
360+
"cabin test testname filters multiple tests"_test = [] {
361+
const tests::TempDir tmp;
362+
tests::runCabin({ "new", "testname_project" }, tmp.path).unwrap();
363+
const auto project = tmp.path / "testname_project";
364+
const auto projectPath = tests::fs::weakly_canonical(project).string();
365+
366+
tests::writeFile(project / "src/main.cc",
367+
R"(#include <iostream>
368+
369+
#ifdef CABIN_TEST
370+
void test_function() {
371+
std::cout << "main test function ... ok" << std::endl;
372+
}
373+
374+
int main() {
375+
test_function();
376+
return 0;
377+
}
378+
#else
379+
int main() {
380+
std::cout << "Hello, world!" << std::endl;
381+
return 0;
382+
}
383+
#endif
384+
)");
385+
386+
tests::writeFile(project / "src/TestnameFirst.cc",
387+
R"(#include <iostream>
388+
389+
#ifdef CABIN_TEST
390+
void test_function() {
391+
std::cout << "testname first function ... ok" << std::endl;
392+
}
393+
394+
int main() {
395+
test_function();
396+
return 0;
397+
}
398+
#endif
399+
)");
400+
401+
tests::writeFile(project / "src/TestnameSecond.cc",
402+
R"(#include <iostream>
403+
404+
#ifdef CABIN_TEST
405+
void test_function() {
406+
std::cout << "testname second function ... ok" << std::endl;
407+
}
408+
409+
int main() {
410+
test_function();
411+
return 0;
412+
}
413+
#endif
414+
)");
415+
416+
const auto result =
417+
tests::runCabin({ "test", "Testname" }, project).unwrap();
418+
expect(result.status.success());
419+
420+
auto sanitizedOut = tests::sanitizeOutput(
421+
result.out, { { projectPath, "<PROJECT>" } }); // NOLINT
422+
expect(sanitizedOut
423+
== "testname first function ... ok\n"
424+
"testname second function ... ok\n");
425+
426+
auto sanitizedErr = tests::sanitizeOutput(
427+
result.err, { { projectPath, "<PROJECT>" } }); // NOLINT
428+
expect(sanitizedErr
429+
== expectedTestSummary(
430+
TestInfo{ .projectName = "testname_project",
431+
.testTargets = { "TestnameFirst", "TestnameSecond" },
432+
.numPassed = 2,
433+
.numFailed = 0,
434+
.numFiltered = 1 }));
435+
};
263436
}

0 commit comments

Comments
 (0)