diff --git a/cli/dispatcher/internal/compilecheck/fast_path_test.go b/cli/dispatcher/internal/compilecheck/fast_path_test.go index 88c7fc273..dc651ba5b 100644 --- a/cli/dispatcher/internal/compilecheck/fast_path_test.go +++ b/cli/dispatcher/internal/compilecheck/fast_path_test.go @@ -183,8 +183,9 @@ func TestSmallAssemblyIsNotTried(t *testing.T) { } } -// Verifies that an input other than source contents that changed while csc compiled leaves no -// baseline, and says why, so the next body edit compiles in full. +// Verifies that an input other than source contents that changed while csc compiled leaves neither a +// baseline nor a record of the compile to reuse, and the note says why, so the next body edit +// compiles in full. func TestNonSourceInputChangedDuringCompileLeavesNoBaseline(t *testing.T) { project := newFastPathProject(t, fastPathMinimumSources) recorder := newCachedSchedulerRecorder("Alpha", "Beta") @@ -205,7 +206,7 @@ func TestNonSourceInputChangedDuringCompileLeavesNoBaseline(t *testing.T) { project.editBody(t, 0, "edited") second := newCachedSchedulerRecorder("Alpha", "Beta") - assertFallback(t, project.run(t, second, false), second, "no baseline: the last full compile did not record one") + assertFallback(t, project.run(t, second, false), second, "no baseline: no full compile of this assembly is on record") } // Verifies that a baseline whose saved generator output was deleted or changed is not used, since diff --git a/cli/dispatcher/internal/compilecheck/result_cache.go b/cli/dispatcher/internal/compilecheck/result_cache.go index 4e8e8c97f..4592285d0 100644 --- a/cli/dispatcher/internal/compilecheck/result_cache.go +++ b/cli/dispatcher/internal/compilecheck/result_cache.go @@ -58,9 +58,15 @@ func (c Compiler) reuseOrCompile( if err != nil || key == "" || result.RawOutput != "" { return result, err } - if writeErr := records.RecordCompile(unit.Assembly.AssemblyName, key, - resultstore.CompileResult[Diagnostic]{Succeeded: result.Succeeded, Diagnostics: result.Diagnostics}, unitOutputNames(unit)); writeErr != nil { - return UnitResult{}, writeErr + // Why the key is read again before recording: csc reads the sources while it compiles, so an edit + // made during the compile would otherwise be recorded under the key of the bytes before it, and + // putting the file back would replay what the edited file reported. CompileUnit has already + // removed the previous record, so leaving this one out makes the next run compile again. + if c.keyStillHolds(plan, unit, key) { + if writeErr := records.RecordCompile(unit.Assembly.AssemblyName, key, + resultstore.CompileResult[Diagnostic]{Succeeded: result.Succeeded, Diagnostics: result.Diagnostics}, unitOutputNames(unit)); writeErr != nil { + return UnitResult{}, writeErr + } } // Why --full records too: running --full before finishing would otherwise drop the baseline, and diff --git a/cli/dispatcher/internal/compilecheck/scheduler_cache_test.go b/cli/dispatcher/internal/compilecheck/scheduler_cache_test.go index 27661be28..fb66ff6e8 100644 --- a/cli/dispatcher/internal/compilecheck/scheduler_cache_test.go +++ b/cli/dispatcher/internal/compilecheck/scheduler_cache_test.go @@ -544,3 +544,35 @@ func TestCompileUnitsRecordsNothingWhenASuccessfulCompileWroteNoOutput(t *testin t.Errorf("the next run should have compiled the unit, got %d runs", recorder.runs["Alpha"]) } } + +// Verifies a compile of sources edited while csc ran is not recorded under the key of the bytes +// before the edit, so putting those bytes back compiles them again rather than reusing what the +// edited file reported. +func TestCompileUnitsDoesNotRecordACompileOfSourcesEditedDuringIt(t *testing.T) { + project := newCachedSchedulerProject(t, nil, "Alpha") + project.compileOrFail(t, newCachedSchedulerRecorder("Alpha")) + project.editSource(t, "Alpha", "class Alpha { void X() { } }") + edited := newCachedSchedulerRecorder("Alpha") + var editErr error + edited.during = func(name string) { + if name == "Alpha" { + editErr = os.WriteFile(filepath.Join(project.root, "Assets", "Alpha", "Alpha.cs"), + []byte("class Alpha { void Y() { } }"), 0o600) + } + } + project.compileOrFail(t, edited) + if editErr != nil { + t.Fatalf("failed to edit the source during the compile: %v", editErr) + } + if edited.runs["Alpha"] != 1 { + t.Fatalf("expected the edited unit to compile, got %d runs", edited.runs["Alpha"]) + } + project.editSource(t, "Alpha", "class Alpha { void X() { } }") + + recorder := newCachedSchedulerRecorder("Alpha") + project.compileOrFail(t, recorder) + + if recorder.runs["Alpha"] != 1 { + t.Errorf("expected the reverted source to compile again, got %d runs", recorder.runs["Alpha"]) + } +} diff --git a/docs/compile-check.md b/docs/compile-check.md index e404c9b8e..fe3eb77b8 100644 --- a/docs/compile-check.md +++ b/docs/compile-check.md @@ -152,8 +152,9 @@ build - does not call the worker again: it reports what the previous check repor Otherwise the assembly is compiled in full as before, and `FastPathFallbacks` says why: -- `no baseline: no full compile of this assembly is on record` (none ran yet, or a later run - skipped or blocked the assembly, which removes its result and baseline) +- `no baseline: no full compile of this assembly is on record` (none ran yet, a later run + skipped or blocked the assembly, which removes its result and baseline, or an input changed while + the last full compile ran, so it left no record; `FastPathBaselineNotes` of that run says which) - `no baseline: the last full compile had errors` (a full compile that fails drops the baseline an earlier one left) - `no baseline: the last full compile did not record one` (`FastPathBaselineNotes` of that run @@ -176,8 +177,9 @@ Otherwise the assembly is compiled in full as before, and `FastPathFallbacks` sa not support) A full compile that succeeds records a new baseline, `--full` runs included, so the run after it can -use the check again. A source or another input edited while csc was compiling leaves no baseline, -and `FastPathBaselineNotes` says why a successful full compile left none. An input edited while the +use the check again. A source or another input edited while csc was compiling leaves no baseline +and no record of that compile to reuse, so the next run compiles the assembly again; +`FastPathBaselineNotes` says why a successful full compile left no baseline. An input edited while the changed files were being checked leaves no record of that check, so the next run checks them again. So the first body edit is compiled in full more often than it looks: