-
Notifications
You must be signed in to change notification settings - Fork 251
init: Skip devcontainer.json write when content unchanged #13655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,10 +119,27 @@ func TestEnsureDevcontainerConfig(t *testing.T) { | |
| } | ||
|
|
||
| // Test that running again doesn't fail (idempotency) | ||
| // Get file stat before second run | ||
| statBefore, err := os.Stat(devcontainerPath) | ||
| if err != nil { | ||
| t.Fatalf("Failed to stat devcontainer.json before second run: %v", err) | ||
| } | ||
|
|
||
| err = ensureDevcontainerConfig(false, []string{}) | ||
| if err != nil { | ||
| t.Fatalf("ensureDevcontainerConfig() should be idempotent, but failed: %v", err) | ||
| } | ||
|
|
||
| // Get file stat after second run | ||
| statAfter, err := os.Stat(devcontainerPath) | ||
| if err != nil { | ||
| t.Fatalf("Failed to stat devcontainer.json after second run: %v", err) | ||
| } | ||
|
|
||
| // File modification time should be the same (file should not have been rewritten) | ||
| if !statBefore.ModTime().Equal(statAfter.ModTime()) { | ||
| t.Error("Expected devcontainer.json to not be rewritten when no changes are needed") | ||
| } | ||
| } | ||
|
|
||
| func TestEnsureDevcontainerConfigWithAdditionalRepos(t *testing.T) { | ||
|
|
@@ -768,3 +785,79 @@ func TestGetCurrentRepoName(t *testing.T) { | |
| t.Error("Expected getCurrentRepoName() to return a non-empty string") | ||
| } | ||
| } | ||
|
|
||
| func TestEnsureDevcontainerConfigNoWriteWhenUnchanged(t *testing.T) { | ||
| tmpDir := testutil.TempDir(t, "test-*") | ||
|
|
||
| originalDir, err := os.Getwd() | ||
| if err != nil { | ||
| t.Fatalf("Failed to get current directory: %v", err) | ||
| } | ||
| defer func() { | ||
| _ = os.Chdir(originalDir) | ||
| }() | ||
|
|
||
| if err := os.Chdir(tmpDir); err != nil { | ||
| t.Fatalf("Failed to change to temp directory: %v", err) | ||
| } | ||
|
|
||
| // Initialize git repo | ||
| if err := exec.Command("git", "init").Run(); err != nil { | ||
| t.Skip("Git not available") | ||
| } | ||
|
|
||
| // Configure git and add remote | ||
| exec.Command("git", "config", "user.name", "Test User").Run() | ||
| exec.Command("git", "config", "user.email", "test@example.com").Run() | ||
| exec.Command("git", "remote", "add", "origin", "https://github.com/testorg/testrepo.git").Run() | ||
|
|
||
| // Create initial devcontainer.json | ||
| err = ensureDevcontainerConfig(false, []string{}) | ||
| if err != nil { | ||
| t.Fatalf("Initial ensureDevcontainerConfig() failed: %v", err) | ||
| } | ||
|
|
||
| devcontainerPath := filepath.Join(".devcontainer", "devcontainer.json") | ||
|
|
||
| // Get file info after first write | ||
| firstStat, err := os.Stat(devcontainerPath) | ||
| if err != nil { | ||
| t.Fatalf("Failed to stat file after first write: %v", err) | ||
| } | ||
| firstModTime := firstStat.ModTime() | ||
|
|
||
| // Read the first content | ||
| firstContent, err := os.ReadFile(devcontainerPath) | ||
| if err != nil { | ||
| t.Fatalf("Failed to read file after first write: %v", err) | ||
| } | ||
|
|
||
| // Run again with same parameters - should not write | ||
| err = ensureDevcontainerConfig(false, []string{}) | ||
| if err != nil { | ||
| t.Fatalf("Second ensureDevcontainerConfig() failed: %v", err) | ||
| } | ||
|
|
||
| // Get file info after second run | ||
| secondStat, err := os.Stat(devcontainerPath) | ||
| if err != nil { | ||
| t.Fatalf("Failed to stat file after second run: %v", err) | ||
| } | ||
| secondModTime := secondStat.ModTime() | ||
|
|
||
| // Read the second content | ||
| secondContent, err := os.ReadFile(devcontainerPath) | ||
| if err != nil { | ||
| t.Fatalf("Failed to read file after second run: %v", err) | ||
| } | ||
|
|
||
| // Modification times should be equal (file was not rewritten) | ||
| if !firstModTime.Equal(secondModTime) { | ||
| t.Errorf("File was rewritten when no changes were needed. First modtime: %v, Second modtime: %v", firstModTime, secondModTime) | ||
| } | ||
|
|
||
| // Content should be identical | ||
| if string(firstContent) != string(secondContent) { | ||
| t.Error("File content changed when it should not have") | ||
| } | ||
| } | ||
|
Comment on lines
+789
to
+863
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comparison logic flaw: The comparison compares the newly computed config (after merge operations) against existingConfig. However, due to a shallow copy on line 112 (config = *existingConfig), the pointer fields (Customizations, Build) and map field (Features) are shared between config and existingConfig. When these shared structures are modified through config (lines 114-160), existingConfig is also mutated. This causes the comparison to always succeed because both variables point to the same mutated data, making the skip-write optimization ineffective for detecting actual changes.
A more robust approach would be to compare against the original file content before any mutations, or preserve the original existingConfig through deep copying before mutations.