Skip to content

Commit 223ed4e

Browse files
test: Add support for multifile tests. (#38)
1 parent 65d19fb commit 223ed4e

File tree

9 files changed

+172
-42
lines changed

9 files changed

+172
-42
lines changed

test/helpers/expectations.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ bool addToExpectations(Expectations &exp, string_view filePath, bool isDirectory
7777
exp.minimizeRBI = filePath;
7878
return true;
7979
} else if (absl::EndsWith(filePath, ".rb") || absl::EndsWith(filePath, ".rbi")) {
80-
if (!absl::EndsWith(filePath, ".snapshot.rb")) {
80+
if (!absl::EndsWith(filePath, ".snapshot.rb") && !absl::EndsWith(filePath, "scip-ruby-args.rb")) {
8181
exp.sourceFiles.emplace_back(filePath);
8282
return true;
8383
}
@@ -132,6 +132,7 @@ Expectations getExpectationsForFolderTest(string_view dir) {
132132
ENFORCE(dir.back() == '/');
133133

134134
Expectations exp;
135+
exp.isFolderTest = true;
135136
// No basename; all of these files belong to this expectations.
136137
exp.basename = "";
137138
exp.folder = dir;
@@ -149,6 +150,7 @@ Expectations getExpectationsForTest(string_view parentDir, string_view testName)
149150
vector<string> names = listTrimmedTestFilesInDir(parentDir, false);
150151
bool found = false;
151152
Expectations exp;
153+
exp.isFolderTest = false;
152154
exp.basename = testName.substr(parentDir.size() + 1);
153155
exp.folder = parentDir;
154156
exp.folder += "/";

test/helpers/expectations.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct Expectations {
1919
// expectations type => file => expectations for file
2020
UnorderedMap<std::string, UnorderedMap<std::string, std::string>> expectations;
2121
std::string minimizeRBI;
22+
bool isFolderTest;
2223

2324
static Expectations getExpectations(std::string singleTest);
2425
};

test/scip/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
load(":scip_test.bzl", "scip_test_suite")
22

3-
scip_test_suite(paths = glob(["testdata/*"]))
3+
scip_test_suite(paths = glob(["testdata/*"], exclude_directories = 1), multifile_paths = glob(["testdata/multifile/*/*"]))

test/scip/scip_test.bzl

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
def basename(p):
22
return p.rpartition("/")[-1]
33

4+
def dirname(p):
5+
return p.rpartition("/")[0]
6+
47
def split_extension(p):
58
(before, _, ext) = basename(p).rpartition(".")
69
if before == "":
710
(before, ext) = (ext, "")
811
return (before, ext)
912

10-
def scip_test_suite(paths):
13+
def scip_test_suite(paths, multifile_paths):
1114
tests = []
1215
updates = []
1316
for path in paths:
1417
names = scip_test(path)
1518
if names:
1619
tests.append(names[0])
1720
updates.append(names[1])
21+
22+
file_groups = {}
23+
for p in multifile_paths:
24+
d = dirname(p)
25+
if d in file_groups:
26+
file_groups[d].append(p)
27+
else:
28+
file_groups[d] = [p]
29+
30+
for dir, files in file_groups.items():
31+
names = scip_multifile_test(dir, files)
32+
tests.append(names[0])
33+
updates.append(names[1])
34+
1835
native.test_suite(
1936
name = "scip",
2037
tests = tests,
@@ -24,22 +41,34 @@ def scip_test_suite(paths):
2441
tests = updates,
2542
)
2643

27-
2844
def scip_test(path):
29-
# path will end in either .snapshot, .rb or have no extension.
30-
31-
filename, ext = split_extension(path)
32-
if ext != "rb":
33-
# TODO(varun): Add support for folder tests, when there is no extension
45+
if not path.endswith(".rb") or path.endswith(".snapshot.rb"):
3446
return None
35-
test_name, other_ext = split_extension(filename)
36-
if other_ext != "":
37-
# Don't make separate tests for .snapshot.rb files
38-
return None
39-
47+
test_name = basename(path)[:-3]
4048
snapshot_path = path[:-3] + ".snapshot.rb"
4149
args = ["$(location {})".format(path), "--output=$(location {})".format(snapshot_path)]
4250
data = [path, snapshot_path, "//test:scip_test_runner"]
51+
return _make_test(test_name, args, data)
52+
53+
def scip_multifile_test(dir, filepaths):
54+
args = ["$(location {})".format(dir), "--output=$(location {})".format(dir)]
55+
data = ["//test:scip_test_runner", "//test/scip:{}".format(dir)]
56+
for filepath in filepaths:
57+
if filepath.endswith(".rb") and not filepath.endswith(".snapshot.rb"):
58+
# args.append("$(location {})".format(path))
59+
data.append(filepath)
60+
if not filepath.endswith("scip-ruby-args.rb"): # Special file for reading Gem-level args.
61+
data.append(filepath[:-3] + ".snapshot.rb")
62+
if not dir.startswith("testdata/multifile/"):
63+
fail("Expected directory to be under multifile/")
64+
dir = dir[len("testdata/multifile/"):]
65+
if "/" in dir:
66+
fail("Expected directory to be 1-level deep under multifile/")
67+
68+
test_name = "multifile_" + dir
69+
return _make_test(test_name, args, data)
70+
71+
def _make_test(test_name, args, data):
4372
native.sh_test(
4473
name = test_name,
4574
srcs = ["scip_test_runner.sh"],
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# typed: true
2+
3+
class C1
4+
extend T::Sig
5+
6+
sig { returns(T::Boolean) }
7+
def m1
8+
true
9+
end
10+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# typed: true
2+
3+
class C1
4+
# ^^ definition [..] C1#
5+
extend T::Sig
6+
# ^ reference [..] T#
7+
# ^^^ reference [..] T#Sig#
8+
9+
sig { returns(T::Boolean) }
10+
# ^^^ reference [..] Sorbet#Private#Static#<Class:ResolvedSig>#sig().
11+
# ^^^^^^^ reference [..] T#Private#Methods#DeclBuilder#returns().
12+
# ^ reference [..] T#
13+
# ^^^^^^^ reference [..] T#Boolean.
14+
# ^^^^^^^^^^ reference [..] Sorbet#Private#Static#ResolvedSig#
15+
def m1
16+
# ^^^^^^ definition [..] C1#m1().
17+
true
18+
end
19+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# typed: true
2+
# options: showDocs
3+
4+
require 'def_class1'
5+
6+
b = C1.new.m1
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# typed: true
2+
# options: showDocs
3+
4+
require 'def_class1'
5+
6+
b = C1.new.m1
7+
#^ definition local 2~#119448696
8+
#documentation
9+
#| ```ruby
10+
#| b = T.let(_, T::Boolean)
11+
#| ```
12+
# ^^ reference [..] C1#
13+
# ^^ reference [..] C1#m1().

test/scip_test_runner.cc

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ namespace sorbet::test {
5353
using namespace std;
5454

5555
bool update;
56-
vector<string> inputs;
57-
string output;
56+
string inputFileOrDir;
57+
string outputFileOrDir;
5858

5959
// Copied from pipeline_test_runner.cc
6060
class CFGCollectorAndTyper { // TODO(varun): Copy this over to scip_test_runner.cc
@@ -252,19 +252,27 @@ string snapshot_path(string rb_path) {
252252
rb_path.erase(rb_path.size() - 3, 3);
253253
return rb_path + ".snapshot.rb";
254254
}
255+
struct TestSettings {
256+
optional<string> gemMetadata;
257+
UnorderedMap</*root-relative path*/ string, FormatOptions> formatOptions;
258+
};
255259

256-
void updateSnapshots(const scip::Index &index, FormatOptions options, const std::filesystem::path &outputDir) {
260+
void updateSnapshots(const scip::Index &index, const TestSettings &settings, const std::filesystem::path &outputDir) {
257261
for (auto &doc : index.documents()) {
258262
auto outputFilePath = snapshot_path(doc.relative_path());
259263
ofstream out(outputFilePath);
260264
if (!out.is_open()) {
261265
FAIL(fmt::format("failed to open snapshot output file at {}", outputFilePath));
262266
}
263-
formatSnapshot(doc, options, out);
267+
auto it = settings.formatOptions.find(doc.relative_path());
268+
ENFORCE(it != settings.formatOptions.end(),
269+
fmt::format("missing path {} as key in formatOptions map", doc.relative_path()));
270+
formatSnapshot(doc, it->second, out);
264271
}
265272
}
266273

267-
void compareSnapshots(const scip::Index &index, FormatOptions options, const std::filesystem::path &snapshotDir) {
274+
void compareSnapshots(const scip::Index &index, const TestSettings &settings,
275+
const std::filesystem::path &snapshotDir) {
268276
for (auto &doc : index.documents()) {
269277
auto filePath = snapshot_path(doc.relative_path()); // TODO: Separate out folders!
270278
ifstream inputStream(filePath);
@@ -275,7 +283,10 @@ void compareSnapshots(const scip::Index &index, FormatOptions options, const std
275283
input << inputStream.rdbuf();
276284

277285
ostringstream out;
278-
formatSnapshot(doc, options, out);
286+
auto it = settings.formatOptions.find(doc.relative_path());
287+
ENFORCE(it != settings.formatOptions.end(),
288+
fmt::format("missing path {} as key in formatOptions map", doc.relative_path()));
289+
formatSnapshot(doc, it->second, out);
279290
auto result = out.str();
280291

281292
CHECK_EQ_DIFF(input.str(), result,
@@ -304,17 +315,11 @@ pair<optional<string>, FormatOptions> readMagicComments(string_view path) {
304315
return {gemMetadata, options};
305316
}
306317

307-
TEST_CASE("SCIPTest") {
308-
// FIXME(varun): Add support for multifile tests.
309-
ENFORCE(inputs.size() == 1);
310-
Expectations test = Expectations::getExpectations(inputs[0]);
311-
312-
auto [gemMetadata, formatOptions] = readMagicComments(inputs[0]);
313-
318+
void test_one_gem(Expectations &test, const TestSettings &settings) {
314319
vector<unique_ptr<core::Error>> errors;
315-
auto inputPath = test.folder + test.basename;
316320

317-
auto logger = spdlog::stderr_color_mt("fixtures: " + inputPath);
321+
auto logger =
322+
spdlog::stderr_color_mt("fixtures: " + (test.isFolderTest ? test.folder + test.basename : test.folder));
318323
auto errorCollector = make_shared<core::ErrorCollector>();
319324
auto errorQueue = make_shared<core::ErrorQueue>(*logger, *logger, errorCollector);
320325
core::GlobalState gs(errorQueue);
@@ -335,7 +340,7 @@ TEST_CASE("SCIPTest") {
335340
using Provider = pipeline::semantic_extension::SemanticExtensionProvider;
336341

337342
auto indexFilePath = filesystem::temp_directory_path();
338-
indexFilePath.append(test.basename + ".scip"); // FIXME(varun): Update for folder tests with multiple files?
343+
indexFilePath.append(test.basename + ".scip");
339344

340345
auto providers = Provider::getProviders();
341346
ENFORCE(providers.size() == 1);
@@ -344,10 +349,10 @@ TEST_CASE("SCIPTest") {
344349
cxxopts::Options options{"scip-ruby-snapshot-test"};
345350
scipProvider->injectOptions(options);
346351
std::vector<const char *> argv = {"scip-ruby-snapshot-test", "--index-file", indexFilePath.c_str()};
347-
if (gemMetadata.has_value()) {
352+
if (settings.gemMetadata.has_value()) {
348353
argv.push_back("--gem-metadata");
349-
ENFORCE(!gemMetadata.value().empty());
350-
argv.push_back(gemMetadata.value().data());
354+
ENFORCE(!settings.gemMetadata.value().empty());
355+
argv.push_back(settings.gemMetadata.value().data());
351356
}
352357
argv.push_back(nullptr);
353358
auto parseResult = options.parse(argv.size() - 1, argv.data());
@@ -404,14 +409,60 @@ TEST_CASE("SCIPTest") {
404409
index.ParseFromIstream(&indexFile);
405410

406411
if (update) {
407-
updateSnapshots(index, formatOptions, test.folder);
412+
updateSnapshots(index, settings, test.folder);
408413
} else {
409-
compareSnapshots(index, formatOptions, test.folder);
414+
compareSnapshots(index, settings, test.folder);
410415
}
411416

412417
MESSAGE("PASS");
413418
}
414419

420+
// There are several different kinds of tests that we are potentially
421+
// interested in here:
422+
//
423+
// 1. Testing single files for language features.
424+
// 2. Testing multiple files within the same Gem to see if/when defs/refs work.
425+
// 3. Testing usage of the --gem-metadata flag, because not all Ruby Gems
426+
// contain a .gemspec file (e.g. if it's an application), which means
427+
// that we do not have a reliable way of inferring the name/version.
428+
// 4. Testing usage of Gem-internal RBI files.
429+
// 5. Testing usage of RBI files coming from external gems. This uses
430+
// a standardized project structure with a sorbet/ folder.
431+
// (See https://sorbet.org/docs/rbi)
432+
// 6. Testing usage of multiple local Gems defined within the same repo.
433+
//
434+
// Right now, the testing is set up to handle use cases 1, 2, 3, and 4.
435+
// It should hopefully be straightforward to extend this support for 5.
436+
//
437+
// For 6, I'd like to gather more information about project setups first
438+
// (the directory layout, any expectations of ordering, how Sorbet is run)
439+
// before setting up testing infrastructure and explicit feature support.
440+
TEST_CASE("SCIPTest") {
441+
Expectations test = Expectations::getExpectations(inputFileOrDir);
442+
443+
TestSettings settings;
444+
settings.gemMetadata = nullopt;
445+
if (test.isFolderTest) {
446+
auto argsFilePath = test.folder + "scip-ruby-args.rb";
447+
if (FileOps::exists(argsFilePath)) {
448+
settings.gemMetadata = readMagicComments(argsFilePath).first;
449+
}
450+
ENFORCE(test.sourceFiles.size() > 0);
451+
for (auto &sourceFile : test.sourceFiles) {
452+
auto path = test.folder + sourceFile;
453+
auto [_, inserted] = settings.formatOptions.insert({path, readMagicComments(path).second});
454+
ENFORCE(inserted, "duplicate source file in Expectations struct?");
455+
}
456+
} else {
457+
ENFORCE(test.sourceFiles.size() == 1);
458+
auto path = test.folder + test.sourceFiles[0];
459+
auto &options = settings.formatOptions[path];
460+
std::tie(settings.gemMetadata, options) = readMagicComments(path);
461+
}
462+
463+
test_one_gem(test, settings);
464+
}
465+
415466
} // namespace sorbet::test
416467

417468
// TODO: Use string.ends_with with C++20.
@@ -435,17 +486,16 @@ int main(int argc, char *argv[]) {
435486
"path")("update-snapshots", "");
436487
//("--update-snapshots", "should the snapshot files be overwritten if there are changes");
437488
auto res = options.parse(argc, argv);
489+
auto unmatched = res.unmatched();
438490

439-
for (auto &input : res.unmatched()) {
440-
if (!ends_with(input, ".rb")) {
441-
printf("error: input files must have .rb extension");
442-
return 1;
443-
}
491+
if (unmatched.size() != 1) {
492+
std::fprintf(stderr, "error: runner expected single input file with .rb extension or folder");
493+
return 1;
444494
}
445495

446496
sorbet::test::update = res.count("update-snapshots") > 0;
447-
sorbet::test::inputs = res.unmatched();
448-
sorbet::test::output = res["output"].as<std::string>();
497+
sorbet::test::inputFileOrDir = unmatched[0];
498+
sorbet::test::outputFileOrDir = res["output"].as<std::string>();
449499

450500
doctest::Context context(argc, argv);
451501
return context.run();

0 commit comments

Comments
 (0)