Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 4 additions & 58 deletions cli/dispatcher/internal/compilecheck/baseline_record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ func TestBaselineRecordedAfterSuccessfulCompile(t *testing.T) {

project.run(t, newCachedSchedulerRecorder("Alpha", "Beta"), false)

record := project.readBaseline(t)
record := project.baseline(t)
keyResult, err := computeInputKey(project.root, project.paths, project.plan, project.plan.Units[0])
if err != nil {
t.Fatalf("expected a key, got error: %v", err)
}
if record.InputKey != project.readManifest(t).InputKey || record.InputKey != keyResult.Key {
// Why a usable baseline shows it carries the manifest's input key: the store hands one back only
// when the two keys agree.
if record.InputKey != keyResult.Key {
t.Errorf("expected the baseline to carry the manifest's input key, got %s", record.InputKey)
}
for source, hash := range keyResult.SourceHashes {
Expand Down Expand Up @@ -129,27 +131,6 @@ func TestCompileUnitPassesGeneratedFilesOut(t *testing.T) {
}
}

// Verifies that removing a unit's outputs also removes its baseline and its saved generator output.
func TestRemoveUnitOutputsRemovesBaselineAndGeneratedDirectory(t *testing.T) {
outputDirectoryPath, unit := newManifestUnit(t)
writeOutputFile(t, outputDirectoryPath, "Alpha"+baselineRecordExtension, "{}")
generatedPath := filepath.Join(outputDirectoryPath, "Alpha"+generatedDirectoryExtension, "Nested")
if err := os.MkdirAll(generatedPath, outputDirPermissions); err != nil {
t.Fatalf("failed to create the generated directory: %v", err)
}
writeOutputFile(t, generatedPath, "G.cs", "class G {}")

if err := (Compiler{ProjectRoot: outputDirectoryPath}).removeUnitOutputs(BuildPlan{}, unit); err != nil {
t.Fatalf("expected the outputs to be removed, got error: %v", err)
}

for _, name := range []string{"Alpha" + baselineRecordExtension, "Alpha" + generatedDirectoryExtension} {
if _, err := os.Stat(filepath.Join(outputDirectoryPath, name)); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed, got %v", name, err)
}
}
}

// Verifies that the worker build is shared by later callers, and that a cancelled build is tried again.
func TestOnceWithContextSharesTheAnswerButNotACancellation(t *testing.T) {
calls := 0
Expand Down Expand Up @@ -269,38 +250,3 @@ func TestRecordBaselineRefusesAMissingNonSourceKey(t *testing.T) {
t.Errorf("expected no baseline without a non-source key, got %v", err)
}
}

// Verifies that only generated C# files are recorded, keyed relative to the generator output directory.
func TestHashGeneratedFilesRecordsOnlyCSharpFiles(t *testing.T) {
directory := t.TempDir()
if err := os.MkdirAll(filepath.Join(directory, "Gen"), outputDirPermissions); err != nil {
t.Fatalf("failed to create the generator folder: %v", err)
}
writeOutputFile(t, filepath.Join(directory, "Gen"), "A.g.cs", "class A { }")
writeOutputFile(t, filepath.Join(directory, "Gen"), "notes.txt", "not code")

hashes, err := hashGeneratedFiles(directory)

if err != nil || len(hashes) != 1 || hashes["Gen/A.g.cs"] == "" {
t.Errorf("expected only Gen/A.g.cs, got %v (%v)", hashes, err)
}
if missing, err := hashGeneratedFiles(filepath.Join(directory, "missing")); err != nil || len(missing) != 0 {
t.Errorf("expected a missing directory to hold no files, got %v (%v)", missing, err)
}
}

// Verifies that a baseline another version of compile-check wrote is not used, and says so rather
// than reporting that no full compile ran.
func TestBaselineOfAnotherFormatFallsBack(t *testing.T) {
project := newFastPathProject(t, fastPathMinimumSources)
project.recordBaseline(t)
record := project.readBaseline(t)
record.FormatVersion = baselineRecordFormatVersion - 1
project.writeJSON(t, project.baselinePath(), record)
project.editBody(t, 0, "edited")
recorder := newCachedSchedulerRecorder("Alpha", "Beta")

outcome := project.run(t, recorder, false)

assertFallback(t, outcome, recorder, "no baseline: it was recorded by another version of compile-check")
}
99 changes: 48 additions & 51 deletions cli/dispatcher/internal/compilecheck/fast_path_fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -221,74 +221,71 @@ func (project *fastPathProject) recordBaseline(t *testing.T) {
}
}

// baselinePath names Alpha's baseline record.
func (project *fastPathProject) baselinePath() string {
return filepath.Join(project.outputDirectory(), "Alpha"+baselineRecordExtension)
}

// hasBaseline reports whether Alpha has a baseline record.
// hasBaseline reports whether Alpha has a baseline of the current format.
func (project *fastPathProject) hasBaseline() bool {
_, err := os.Stat(project.baselinePath())

return err == nil
}

// readBaseline reads Alpha's baseline record.
func (project *fastPathProject) readBaseline(t *testing.T) baselineRecord {
t.Helper()
record := baselineRecord{}
project.readJSON(t, project.baselinePath(), &record)

return record
return project.hasBaselineOf("Alpha")
}

// readManifest reads Alpha's result manifest.
func (project *fastPathProject) readManifest(t *testing.T) unitResultManifest {
// baseline reads Alpha's baseline through the store and fails the test when there is none to use.
func (project *fastPathProject) baseline(t *testing.T) storedBaseline {
t.Helper()
manifest := unitResultManifest{}
project.readJSON(t, project.manifestPathOf("Alpha"), &manifest)
baseline, reason := project.records().loadBaseline("Alpha")
if reason != "" {
t.Fatalf("expected a usable baseline for Alpha, got %q", reason)
}

return manifest
return baseline
}

func (project *fastPathProject) readJSON(t *testing.T, path string, value any) {
// generatedFileCount counts the files under the generator output a baseline points at.
func (project *fastPathProject) generatedFileCount(t *testing.T, baseline storedBaseline) int {
t.Helper()
content, err := os.ReadFile(path)
count := 0
err := filepath.WalkDir(projectPath(project.root, baseline.GeneratedDirectory),
func(_ string, entry fs.DirEntry, err error) error {
if err == nil && !entry.IsDir() {
count++
}

return err
})
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
t.Fatalf("failed to read the generator output: %v", err)
}
if err := json.Unmarshal(content, value); err != nil {
t.Fatalf("failed to parse %s: %v", path, err)
}
}

func (project *fastPathProject) writeJSON(t *testing.T, path string, value any) {
t.Helper()
content, err := json.Marshal(value)
if err != nil {
t.Fatalf("failed to encode %s: %v", path, err)
}
if err := os.WriteFile(path, content, 0o600); err != nil {
t.Fatalf("failed to write %s: %v", path, err)
}
return count
}

// fileHashes hashes Alpha's outputs, manifest, and baseline, to show a run left them alone.
func (project *fastPathProject) fileHashes(t *testing.T) map[string]string {
// outputSnapshot hashes every file the check keeps for Alpha, keyed by its path relative to the
// output directory, leaving out the response file the changed-files check writes for itself.
func (project *fastPathProject) outputSnapshot(t *testing.T) map[string]string {
t.Helper()
hashes := map[string]string{}
for _, name := range []string{
"Alpha" + assemblyExtension, "Alpha" + referenceAssemblyExtension,
"Alpha" + resultManifestExtension, "Alpha" + baselineRecordExtension,
} {
hash, err := hashFile(filepath.Join(project.outputDirectory(), name))
snapshot := map[string]string{}
err := filepath.WalkDir(project.outputDirectory(), func(path string, entry fs.DirEntry, err error) error {
if err != nil || entry.IsDir() {
return err
}
relative, err := filepath.Rel(project.outputDirectory(), path)
if err != nil {
t.Fatalf("failed to hash %s: %v", name, err)
return err
}
hashes[name] = hash
first, _, _ := strings.Cut(filepath.ToSlash(relative), "/")
if !strings.HasPrefix(first, "Alpha.") || first == "Alpha"+fastCheckResponseFileSuffix {
return nil
}
hash, err := hashFile(path)
if err != nil {
return err
}
snapshot[filepath.ToSlash(relative)] = hash

return nil
})
if err != nil {
t.Fatalf("failed to read the output directory: %v", err)
}

return hashes
return snapshot
}

// assertFallback checks that Alpha was compiled in full for a reason starting with prefix.
Expand Down
6 changes: 3 additions & 3 deletions cli/dispatcher/internal/compilecheck/fast_path_order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import (
// matches the baseline, and the check that it is gone could run anywhere in the order unnoticed.
func (project *fastPathProject) recordBaselineWithGeneratorOutput(t *testing.T) string {
t.Helper()
generatedDirectoryPath := openRecordStore(project.root, project.plan.OutputDir).generatedDirectory("Alpha")
generatedDirectoryPath := project.records().generatedDirectory("Alpha")
recorder := newCachedSchedulerRecorder("Alpha", "Beta")
recorder.during = func(unit string) {
if unit == "Alpha" {
project.writeFile(t, filepath.Join(generatedDirectoryPath, "Gen", "G.g.cs"), "class G { }")
}
}
project.run(t, recorder, false)
if record := project.readBaseline(t); len(record.GeneratedFiles) != 1 {
t.Fatalf("expected the generated file in the baseline, got %v", record.GeneratedFiles)
if count := project.generatedFileCount(t, project.baseline(t)); count != 1 {
t.Fatalf("expected the generated file in the baseline, got %d files", count)
}

return filepath.Join(project.root, generatedDirectoryPath)
Expand Down
61 changes: 34 additions & 27 deletions cli/dispatcher/internal/compilecheck/fast_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ func TestFullFlagSkipsFastPathButRecords(t *testing.T) {
outcome := project.run(t, newCachedSchedulerRecorder("Alpha", "Beta"), true)

assertNotTried(t, outcome, project.worker)
if record, manifest := project.readBaseline(t), project.readManifest(t); record.InputKey != manifest.InputKey {
t.Errorf("expected the baseline to describe the --full compile, got %s and %s", record.InputKey, manifest.InputKey)
}
// Why a usable baseline is enough: the store hands one back only when its input key is the one
// the manifest of the last compile carries, and the --full compile removed the earlier pair.
project.baseline(t)
}

// Verifies that a unit picked only as a dependent is neither checked by the changed-files check nor
Expand Down Expand Up @@ -95,7 +95,7 @@ func TestMissingBaselineFallsBackAndRecords(t *testing.T) {
outcome := project.run(t, recorder, false)

assertFallback(t, outcome, recorder, "no baseline: no full compile of this assembly is on record")
record := project.readBaseline(t)
record := project.baseline(t)
if record.WorkerKey != "worker-a" || len(record.Files) != fastPathMinimumSources {
t.Errorf("expected a baseline of every source from worker-a, got key %s with %d files", record.WorkerKey, len(record.Files))
}
Expand All @@ -122,9 +122,11 @@ func TestHandCorruptedFailedManifestFallsBack(t *testing.T) {
project := newFastPathProject(t, fastPathMinimumSources)
project.recordBaseline(t)
project.editBody(t, 0, "edited")
manifest := project.readManifest(t)
manifest.Succeeded = false
project.writeJSON(t, project.manifestPathOf("Alpha"), manifest)
baseline := project.baseline(t)
if err := project.records().recordCompile("Alpha", baseline.InputKey,
recordedCompile{Succeeded: false, Diagnostics: baseline.Diagnostics}, unitOutputNames(project.plan.Units[0])); err != nil {
t.Fatalf("failed to record the failed compile: %v", err)
}
recorder := newCachedSchedulerRecorder("Alpha", "Beta")

outcome := project.run(t, recorder, false)
Expand Down Expand Up @@ -220,16 +222,16 @@ func TestMissingOrChangedGeneratorOutputFallsBack(t *testing.T) {
} {
t.Run(name, func(t *testing.T) {
project := newFastPathProject(t, fastPathMinimumSources)
generatedPath := filepath.Join(project.root, openRecordStore(project.root, project.plan.OutputDir).generatedDirectory("Alpha"))
generatedPath := filepath.Join(project.root, project.records().generatedDirectory("Alpha"))
recorder := newCachedSchedulerRecorder("Alpha", "Beta")
recorder.during = func(unit string) {
if unit == "Alpha" {
project.writeFile(t, filepath.Join(openRecordStore(project.root, project.plan.OutputDir).generatedDirectory("Alpha"), "Gen", "G.g.cs"), "class G { }")
project.writeFile(t, filepath.Join(project.records().generatedDirectory("Alpha"), "Gen", "G.g.cs"), "class G { }")
}
}
project.run(t, recorder, false)
if record := project.readBaseline(t); len(record.GeneratedFiles) != 1 {
t.Fatalf("expected the generated file in the baseline, got %v", record.GeneratedFiles)
if count := project.generatedFileCount(t, project.baseline(t)); count != 1 {
t.Fatalf("expected the generated file in the baseline, got %d files", count)
}
damage(t, generatedPath)
project.editBody(t, 0, "edited")
Expand Down Expand Up @@ -266,7 +268,7 @@ func TestDeclarationChangeFallsBackAndRerecords(t *testing.T) {
outcome := project.run(t, recorder, false)

assertFallback(t, outcome, recorder, fastcheck.DeclarationChangedOutcome+": "+project.sourceName(0))
if file := project.readBaseline(t).Files[project.sourceName(0)]; file.Fingerprint != "class File000 : Base " {
if file := project.baseline(t).Files[project.sourceName(0)]; file.Fingerprint != "class File000 : Base " {
t.Errorf("expected the new declaration in the baseline, got %+v", file)
}
}
Expand Down Expand Up @@ -322,7 +324,7 @@ func TestWorkerKeyMismatchFallsBack(t *testing.T) {
outcome := project.run(t, recorder, false)

assertFallback(t, outcome, recorder, "baseline was fingerprinted by another worker build")
if record := project.readBaseline(t); record.WorkerKey != "worker-b" {
if record := project.baseline(t); record.WorkerKey != "worker-b" {
t.Errorf("expected the baseline to be recorded again by worker-b, got %s", record.WorkerKey)
}
}
Expand All @@ -337,7 +339,7 @@ func TestFastPathCleanResultKeepsBaselineFiles(t *testing.T) {
first.surfaces["Alpha"] = "unity Alpha"
first.output["Alpha"] = fakeCompilerOutput{stdout: "Assets/Alpha/File001.cs(1,1): warning CS0168: unused"}
project.run(t, first, false)
before := project.fileHashes(t)
before := project.outputSnapshot(t)
project.editBody(t, 0, "edited")
project.worker.uncheckedCodes = []string{"CS0169"}

Expand All @@ -353,13 +355,13 @@ func TestFastPathCleanResultKeepsBaselineFiles(t *testing.T) {
if len(result.Diagnostics) != 1 || result.Diagnostics[0].File != "Assets/Alpha/File001.cs" {
t.Errorf("expected the warning of the unchanged file to be carried over, got %+v", result.Diagnostics)
}
if after := project.fileHashes(t); !reflect.DeepEqual(after, before) {
if after := project.outputSnapshot(t); !reflect.DeepEqual(after, before) {
t.Errorf("expected the outputs, manifest, and baseline to stay, got %v then %v", before, after)
}
if recorder.runs["Beta"] != 0 || outcome.Skipped != 1 {
t.Errorf("expected Beta to be skipped, got %d runs and %d skipped", recorder.runs["Beta"], outcome.Skipped)
}
if project.worker.lastCheck.GeneratedDirectory != filepath.Join(project.root, openRecordStore(project.root, project.plan.OutputDir).generatedDirectory("Alpha")) {
if project.worker.lastCheck.GeneratedDirectory != filepath.Join(project.root, project.records().generatedDirectory("Alpha")) {
t.Errorf("expected the saved generator output to be passed, got %q", project.worker.lastCheck.GeneratedDirectory)
}
}
Expand Down Expand Up @@ -430,18 +432,23 @@ func TestRevertedContentIsReused(t *testing.T) {
func TestZeroChangedFilesDropsBaselineAndFallsBack(t *testing.T) {
project := newFastPathProject(t, fastPathMinimumSources)
project.recordBaseline(t)
record := project.readBaseline(t)
manifest := project.readManifest(t)
record.InputKey = "tampered"
manifest.InputKey = "tampered"
project.writeJSON(t, project.baselinePath(), record)
project.writeJSON(t, project.manifestPathOf("Alpha"), manifest)
baseline := project.baseline(t)
records := project.records()
if err := records.recordCompile("Alpha", "tampered",
recordedCompile{Succeeded: true, Diagnostics: baseline.Diagnostics}, unitOutputNames(project.plan.Units[0])); err != nil {
t.Fatalf("failed to record the tampered compile: %v", err)
}
if err := records.recordBaseline("Alpha", baselineInputs{
InputKey: "tampered", NonSourceKey: baseline.NonSourceKey, WorkerKey: baseline.WorkerKey, Files: baseline.Files,
}); err != nil {
t.Fatalf("failed to record the tampered baseline: %v", err)
}
recorder := newCachedSchedulerRecorder("Alpha", "Beta")

outcome := project.run(t, recorder, false)

assertFallback(t, outcome, recorder, "baseline record does not match its compile result")
if project.readBaseline(t).InputKey == "tampered" {
if project.baseline(t).InputKey == "tampered" {
t.Error("expected the disagreeing baseline to be replaced")
}
}
Expand All @@ -451,7 +458,7 @@ func TestZeroChangedFilesDropsBaselineAndFallsBack(t *testing.T) {
func TestCancelledFastPathLeavesBaselineUntouched(t *testing.T) {
project := newFastPathProject(t, fastPathMinimumSources)
project.recordBaseline(t)
before := project.fileHashes(t)
before := project.outputSnapshot(t)
project.editBody(t, 0, "edited")
ctx, cancel := context.WithCancel(context.Background())
project.worker.onCheck = cancel
Expand All @@ -462,7 +469,7 @@ func TestCancelledFastPathLeavesBaselineUntouched(t *testing.T) {
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected the run to stop with a cancellation, got %v", err)
}
if after := project.fileHashes(t); !reflect.DeepEqual(after, before) {
if after := project.outputSnapshot(t); !reflect.DeepEqual(after, before) {
t.Errorf("expected the outputs, manifest, and baseline to stay, got %v then %v", before, after)
}
}
Expand Down Expand Up @@ -502,8 +509,8 @@ func TestBaselineRemovedByBlockedRunIsNotCalledNeverCompiled(t *testing.T) {
}
project.plan.Units[1].Sources = sources
project.run(t, newCachedSchedulerRecorder("Alpha", "Beta"), false)
if _, err := os.Stat(filepath.Join(project.outputDirectory(), "Beta"+baselineRecordExtension)); err != nil {
t.Fatalf("expected the first run to record Beta's baseline: %v", err)
if !project.hasBaselineOf("Beta") {
t.Fatal("expected the first run to record Beta's baseline")
}

project.editSource(t, "Alpha", "class Alpha { broken }")
Expand Down
Loading
Loading