From 4b5c233d559e4291ed0d3c31823bdf89e303236e Mon Sep 17 00:00:00 2001 From: Patrick Hoefler Date: Sat, 10 May 2025 21:00:26 +0200 Subject: [PATCH 1/7] test: add integration test --- .gitignore | 1 + internal/cmd/integration_test.go | 107 ++++++++++++++++++++ internal/cmd/testdata/Dockerfile.golden.pdf | Bin 0 -> 20155 bytes 3 files changed, 108 insertions(+) create mode 100644 internal/cmd/integration_test.go create mode 100644 internal/cmd/testdata/Dockerfile.golden.pdf diff --git a/.gitignore b/.gitignore index c6c6b87f..978eafa8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ dockerfilegraph # dockerfilegraph Dockerfile.* !/Dockerfile.alpine +!Dockerfile.golden.pdf !examples/dockerfiles/Dockerfile.large diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go new file mode 100644 index 00000000..d68bf300 --- /dev/null +++ b/internal/cmd/integration_test.go @@ -0,0 +1,107 @@ +package cmd_test + +import ( + "bytes" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/patrickhoefler/dockerfilegraph/internal/cmd" + "github.com/spf13/afero" +) + +func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { + tempDir := t.TempDir() + dockerfilePath := copyExampleDockerfile(t, tempDir) + + originalWorkingDirectory, _ := os.Getwd() + if err := os.Chdir(tempDir); err != nil { + t.Fatalf("failed to change to temp dir: %v", err) + } + defer func() { + if err := os.Chdir(originalWorkingDirectory); err != nil { + t.Fatalf("failed to restore working directory: %v", err) + } + }() + + inputFS := afero.NewOsFs() + outputFile := filepath.Join(tempDir, "Dockerfile.pdf") + buf := runCLI(t, inputFS, dockerfilePath, outputFile) + + info, err := os.Stat(outputFile) + if err != nil { + t.Fatalf("expected output file not found: %v", err) + } + if info.Size() == 0 { + t.Fatalf("output file is empty: %s", outputFile) + } + + if !bytes.Contains(buf.Bytes(), []byte("Successfully created Dockerfile.pdf")) { + t.Errorf("CLI output does not contain success message. Output: %s", buf.String()) + } + + checkGoldenFile(t, outputFile) +} + +func copyExampleDockerfile(t *testing.T, tempDir string) string { + dockerfileSrc := filepath.Join("..", "..", "examples", "dockerfiles", "Dockerfile") + content, err := os.ReadFile(dockerfileSrc) + if err != nil { + t.Fatalf("failed to read example Dockerfile: %v", err) + } + dockerfileDst := filepath.Join(tempDir, "Dockerfile") + if err := os.WriteFile(dockerfileDst, content, 0644); err != nil { + t.Fatalf("failed to write Dockerfile to temp dir: %v", err) + } + return dockerfileDst +} + +func runCLI(t *testing.T, inputFS afero.Fs, dockerfilePath, outputFile string) *bytes.Buffer { + buf := new(bytes.Buffer) + command := cmd.NewRootCmd(buf, inputFS, "dot") + command.SetArgs([]string{"--filename", filepath.Base(dockerfilePath), "--output", "pdf"}) + command.SetOut(buf) + command.SetErr(buf) + + // Set SOURCE_DATE_EPOCH=0 for deterministic PDF output + oldEnv := os.Getenv("SOURCE_DATE_EPOCH") + if err := os.Setenv("SOURCE_DATE_EPOCH", "0"); err != nil { + t.Fatalf("failed to set SOURCE_DATE_EPOCH: %v", err) + } + defer func() { + _ = os.Setenv("SOURCE_DATE_EPOCH", oldEnv) + }() + + if err := command.Execute(); err != nil { + t.Fatalf("CLI execution failed: %v\nOutput: %s", err, buf.String()) + } + return buf +} + +func checkGoldenFile(t *testing.T, outputFile string) { + _, thisFile, _, _ := runtime.Caller(0) + goldenDir := filepath.Join(filepath.Dir(thisFile), "testdata") + goldenFile := filepath.Join(goldenDir, "Dockerfile.golden.pdf") + outputBytes, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("failed to read generated output file: %v", err) + } + if _, err := os.Stat(goldenFile); os.IsNotExist(err) { + if err := os.MkdirAll(goldenDir, 0755); err != nil { + t.Fatalf("failed to create testdata dir: %v", err) + } + if err := os.WriteFile(goldenFile, outputBytes, 0644); err != nil { + t.Fatalf("failed to write golden file: %v", err) + } + t.Logf("golden file did not exist, created: %s", goldenFile) + } else { + goldenBytes, err := os.ReadFile(goldenFile) + if err != nil { + t.Fatalf("failed to read golden file: %v", err) + } + if !bytes.Equal(outputBytes, goldenBytes) { + t.Errorf("output PDF does not match golden file. To update, delete %s and re-run the test.", goldenFile) + } + } +} diff --git a/internal/cmd/testdata/Dockerfile.golden.pdf b/internal/cmd/testdata/Dockerfile.golden.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c94a31e04c290deb3b4a50f27dcf918f1c058a03 GIT binary patch literal 20155 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 Date: Sat, 10 May 2025 21:05:55 +0200 Subject: [PATCH 2/7] refactor: simplify runCLI function signature by removing outputFile parameter --- internal/cmd/integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go index d68bf300..fa234abb 100644 --- a/internal/cmd/integration_test.go +++ b/internal/cmd/integration_test.go @@ -27,7 +27,7 @@ func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { inputFS := afero.NewOsFs() outputFile := filepath.Join(tempDir, "Dockerfile.pdf") - buf := runCLI(t, inputFS, dockerfilePath, outputFile) + buf := runCLI(t, inputFS, dockerfilePath) info, err := os.Stat(outputFile) if err != nil { @@ -57,7 +57,7 @@ func copyExampleDockerfile(t *testing.T, tempDir string) string { return dockerfileDst } -func runCLI(t *testing.T, inputFS afero.Fs, dockerfilePath, outputFile string) *bytes.Buffer { +func runCLI(t *testing.T, inputFS afero.Fs, dockerfilePath string) *bytes.Buffer { buf := new(bytes.Buffer) command := cmd.NewRootCmd(buf, inputFS, "dot") command.SetArgs([]string{"--filename", filepath.Base(dockerfilePath), "--output", "pdf"}) From 3a1b421dc34c8d5ed5bc274881fe2489017fbe9e Mon Sep 17 00:00:00 2001 From: Patrick Hoefler Date: Sat, 10 May 2025 21:34:27 +0200 Subject: [PATCH 3/7] refactor: update integration tests to generate Dockerfile.dot instead of Dockerfile.pdf --- .gitignore | 2 +- internal/cmd/integration_test.go | 56 ++++++--------- internal/cmd/testdata/Dockerfile.golden.dot | 72 ++++++++++++++++++++ internal/cmd/testdata/Dockerfile.golden.pdf | Bin 20155 -> 0 bytes 4 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 internal/cmd/testdata/Dockerfile.golden.dot delete mode 100644 internal/cmd/testdata/Dockerfile.golden.pdf diff --git a/.gitignore b/.gitignore index 978eafa8..be235f58 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,5 @@ dockerfilegraph # dockerfilegraph Dockerfile.* !/Dockerfile.alpine -!Dockerfile.golden.pdf +!Dockerfile.golden.dot !examples/dockerfiles/Dockerfile.large diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go index fa234abb..23d40850 100644 --- a/internal/cmd/integration_test.go +++ b/internal/cmd/integration_test.go @@ -26,22 +26,18 @@ func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { }() inputFS := afero.NewOsFs() - outputFile := filepath.Join(tempDir, "Dockerfile.pdf") - buf := runCLI(t, inputFS, dockerfilePath) - info, err := os.Stat(outputFile) - if err != nil { - t.Fatalf("expected output file not found: %v", err) - } - if info.Size() == 0 { - t.Fatalf("output file is empty: %s", outputFile) - } + // Run CLI to generate Dockerfile.dot + runCLI(t, inputFS, dockerfilePath) + dotFile := filepath.Join(tempDir, "Dockerfile.dot") - if !bytes.Contains(buf.Bytes(), []byte("Successfully created Dockerfile.pdf")) { - t.Errorf("CLI output does not contain success message. Output: %s", buf.String()) + // Read the DOT file generated by the CLI + outputBytes, err := os.ReadFile(dotFile) + if err != nil { + t.Fatalf("failed to read generated dot file: %v", err) } - checkGoldenFile(t, outputFile) + checkGoldenFile(t, outputBytes) } func copyExampleDockerfile(t *testing.T, tempDir string) string { @@ -57,41 +53,28 @@ func copyExampleDockerfile(t *testing.T, tempDir string) string { return dockerfileDst } -func runCLI(t *testing.T, inputFS afero.Fs, dockerfilePath string) *bytes.Buffer { +func runCLI(t *testing.T, inputFS afero.Fs, dockerfilePath string) { buf := new(bytes.Buffer) command := cmd.NewRootCmd(buf, inputFS, "dot") - command.SetArgs([]string{"--filename", filepath.Base(dockerfilePath), "--output", "pdf"}) + command.SetArgs([]string{"--filename", filepath.Base(dockerfilePath), "--output", "dot"}) command.SetOut(buf) command.SetErr(buf) - // Set SOURCE_DATE_EPOCH=0 for deterministic PDF output - oldEnv := os.Getenv("SOURCE_DATE_EPOCH") - if err := os.Setenv("SOURCE_DATE_EPOCH", "0"); err != nil { - t.Fatalf("failed to set SOURCE_DATE_EPOCH: %v", err) - } - defer func() { - _ = os.Setenv("SOURCE_DATE_EPOCH", oldEnv) - }() - if err := command.Execute(); err != nil { - t.Fatalf("CLI execution failed: %v\nOutput: %s", err, buf.String()) + t.Fatalf("CLI execution for DOT failed: %v\nOutput: %s", err, buf.String()) } - return buf } -func checkGoldenFile(t *testing.T, outputFile string) { +func checkGoldenFile(t *testing.T, dotBytes []byte) { _, thisFile, _, _ := runtime.Caller(0) goldenDir := filepath.Join(filepath.Dir(thisFile), "testdata") - goldenFile := filepath.Join(goldenDir, "Dockerfile.golden.pdf") - outputBytes, err := os.ReadFile(outputFile) - if err != nil { - t.Fatalf("failed to read generated output file: %v", err) - } + goldenFile := filepath.Join(goldenDir, "Dockerfile.golden.dot") + if _, err := os.Stat(goldenFile); os.IsNotExist(err) { if err := os.MkdirAll(goldenDir, 0755); err != nil { t.Fatalf("failed to create testdata dir: %v", err) } - if err := os.WriteFile(goldenFile, outputBytes, 0644); err != nil { + if err := os.WriteFile(goldenFile, dotBytes, 0644); err != nil { t.Fatalf("failed to write golden file: %v", err) } t.Logf("golden file did not exist, created: %s", goldenFile) @@ -100,8 +83,13 @@ func checkGoldenFile(t *testing.T, outputFile string) { if err != nil { t.Fatalf("failed to read golden file: %v", err) } - if !bytes.Equal(outputBytes, goldenBytes) { - t.Errorf("output PDF does not match golden file. To update, delete %s and re-run the test.", goldenFile) + if !bytes.Equal(dotBytes, goldenBytes) { + t.Errorf( + "output DOT does not match golden file.\n"+ + "To update, delete %s and re-run the test.\n"+ + "--- Got ---\n%s\n--- Want ---\n%s", + goldenFile, dotBytes, goldenBytes, + ) } } } diff --git a/internal/cmd/testdata/Dockerfile.golden.dot b/internal/cmd/testdata/Dockerfile.golden.dot new file mode 100644 index 00000000..f1f49100 --- /dev/null +++ b/internal/cmd/testdata/Dockerfile.golden.dot @@ -0,0 +1,72 @@ +digraph G { + graph [bb="0,0,504,252", + compound=true, + nodesep=1.00, + rankdir=LR, + ranksep=0.50 + ]; + node [label="\N"]; + external_image_0 [color=grey20, + fontcolor=grey20, + height=0.5, + label="ubuntu:l...887c2c7ac", + pos="72,234", + shape=box, + style="dashed,rounded", + width=2]; + stage_0 [height=0.5, + label=ubuntu, + pos="252,234", + shape=box, + style=rounded, + width=2]; + external_image_0 -> stage_0 [pos="e,179.61,234 144.37,234 152.26,234 160.34,234 168.36,234"]; + stage_2 [fillcolor=grey90, + height=0.5, + label=release, + pos="432,126", + shape=box, + style="filled,rounded", + width=2]; + stage_0 -> stage_2 [arrowhead=empty, + pos="e,400.66,144.41 283.2,215.68 312.86,197.68 358.3,170.11 390.96,150.29", + style=dashed]; + external_image_1 [color=grey20, + fontcolor=grey20, + height=0.5, + label="golang:1...b738433da", + pos="72,126", + shape=box, + style="dashed,rounded", + width=2]; + stage_1 [height=0.5, + label="build-tool-depend...", + pos="252,126", + shape=box, + style=rounded, + width=2]; + external_image_1 -> stage_1 [pos="e,179.61,126 144.37,126 152.26,126 160.34,126 168.36,126"]; + stage_1 -> stage_2 [arrowhead=empty, + pos="e,359.61,126 324.37,126 332.26,126 340.34,126 348.36,126", + style=dashed]; + external_image_2 [color=grey20, + fontcolor=grey20, + height=0.5, + label=buildcache, + pos="72,18", + shape=box, + style="dashed,rounded", + width=2]; + external_image_2 -> stage_1 [arrowhead=ediamond, + pos="e,220.66,107.59 103.2,36.321 132.48,54.086 177.12,81.177 209.69,100.93", + style=dotted]; + external_image_3 [color=grey20, + fontcolor=grey20, + height=0.5, + label=scratch, + pos="252,18", + shape=box, + style="dashed,rounded", + width=2]; + external_image_3 -> stage_2 [pos="e,400.66,107.59 283.2,36.321 312.86,54.319 358.3,81.888 390.96,101.71"]; +} diff --git a/internal/cmd/testdata/Dockerfile.golden.pdf b/internal/cmd/testdata/Dockerfile.golden.pdf deleted file mode 100644 index c94a31e04c290deb3b4a50f27dcf918f1c058a03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20155 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 Date: Sat, 10 May 2025 21:41:53 +0200 Subject: [PATCH 4/7] fix: handle errors when changing working directory in integration test --- internal/cmd/integration_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go index 23d40850..4e86f330 100644 --- a/internal/cmd/integration_test.go +++ b/internal/cmd/integration_test.go @@ -15,12 +15,17 @@ func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { tempDir := t.TempDir() dockerfilePath := copyExampleDockerfile(t, tempDir) - originalWorkingDirectory, _ := os.Getwd() - if err := os.Chdir(tempDir); err != nil { + originalWorkingDirectory, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + err = os.Chdir(tempDir) + if err != nil { t.Fatalf("failed to change to temp dir: %v", err) } defer func() { - if err := os.Chdir(originalWorkingDirectory); err != nil { + err := os.Chdir(originalWorkingDirectory) + if err != nil { t.Fatalf("failed to restore working directory: %v", err) } }() From 75a91f6af0ffd4b2a2dea75f91817fb13a14b476 Mon Sep 17 00:00:00 2001 From: Patrick Hoefler Date: Sat, 10 May 2025 21:54:03 +0200 Subject: [PATCH 5/7] refactor: improve golden file comparison using cmp.Diff for clearer output --- internal/cmd/integration_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go index 4e86f330..08c87801 100644 --- a/internal/cmd/integration_test.go +++ b/internal/cmd/integration_test.go @@ -7,6 +7,7 @@ import ( "runtime" "testing" + "github.com/google/go-cmp/cmp" "github.com/patrickhoefler/dockerfilegraph/internal/cmd" "github.com/spf13/afero" ) @@ -88,12 +89,13 @@ func checkGoldenFile(t *testing.T, dotBytes []byte) { if err != nil { t.Fatalf("failed to read golden file: %v", err) } - if !bytes.Equal(dotBytes, goldenBytes) { + diff := cmp.Diff(string(goldenBytes), string(dotBytes)) + if diff != "" { t.Errorf( "output DOT does not match golden file.\n"+ "To update, delete %s and re-run the test.\n"+ - "--- Got ---\n%s\n--- Want ---\n%s", - goldenFile, dotBytes, goldenBytes, + "Diff (-want +got):\n%s", + goldenFile, diff, ) } } From 3b8be172649486289db83d24bce72a6f988ae02c Mon Sep 17 00:00:00 2001 From: Patrick Hoefler Date: Sun, 11 May 2025 02:13:45 +0200 Subject: [PATCH 6/7] refactor: update integration test to improve Dockerfile.dot generation --- internal/cmd/integration_test.go | 83 ++++++++++----------- internal/cmd/testdata/Dockerfile.golden.dot | 34 ++++----- 2 files changed, 58 insertions(+), 59 deletions(-) diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go index 08c87801..9f7caa0b 100644 --- a/internal/cmd/integration_test.go +++ b/internal/cmd/integration_test.go @@ -1,74 +1,73 @@ package cmd_test import ( - "bytes" + "fmt" "os" + "os/exec" "path/filepath" "runtime" "testing" "github.com/google/go-cmp/cmp" - "github.com/patrickhoefler/dockerfilegraph/internal/cmd" - "github.com/spf13/afero" ) func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { - tempDir := t.TempDir() - dockerfilePath := copyExampleDockerfile(t, tempDir) + // Find the project root directory + _, thisFile, _, _ := runtime.Caller(0) + projectRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "../..")) - originalWorkingDirectory, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get working directory: %v", err) - } - err = os.Chdir(tempDir) + // Build the Linux binary before building the Docker image + makeCmd := exec.Command("make", "build-linux") + makeCmd.Dir = projectRoot + makeOut, err := makeCmd.CombinedOutput() if err != nil { - t.Fatalf("failed to change to temp dir: %v", err) + t.Fatalf("make build-linux failed: %v\n%s", err, string(makeOut)) } - defer func() { - err := os.Chdir(originalWorkingDirectory) - if err != nil { - t.Fatalf("failed to restore working directory: %v", err) - } - }() - - inputFS := afero.NewOsFs() + defer os.Remove(filepath.Join(projectRoot, "dockerfilegraph")) - // Run CLI to generate Dockerfile.dot - runCLI(t, inputFS, dockerfilePath) - dotFile := filepath.Join(tempDir, "Dockerfile.dot") - - // Read the DOT file generated by the CLI - outputBytes, err := os.ReadFile(dotFile) + // Build the Docker image from the project root + buildCmd := exec.Command("docker", "build", "-t", "dockerfilegraph-test", "-f", "Dockerfile", ".") + buildCmd.Dir = projectRoot + buildOut, err := buildCmd.CombinedOutput() if err != nil { - t.Fatalf("failed to read generated dot file: %v", err) + t.Fatalf("docker build failed: %v\n%s", err, string(buildOut)) } - checkGoldenFile(t, outputBytes) -} - -func copyExampleDockerfile(t *testing.T, tempDir string) string { - dockerfileSrc := filepath.Join("..", "..", "examples", "dockerfiles", "Dockerfile") + // Prepare temp dir for output + tempDir := t.TempDir() + // Copy example Dockerfile to temp dir + dockerfileSrc := filepath.Join(projectRoot, "examples", "dockerfiles", "Dockerfile") + dockerfileDst := filepath.Join(tempDir, "Dockerfile") content, err := os.ReadFile(dockerfileSrc) if err != nil { t.Fatalf("failed to read example Dockerfile: %v", err) } - dockerfileDst := filepath.Join(tempDir, "Dockerfile") if err := os.WriteFile(dockerfileDst, content, 0644); err != nil { t.Fatalf("failed to write Dockerfile to temp dir: %v", err) } - return dockerfileDst -} -func runCLI(t *testing.T, inputFS afero.Fs, dockerfilePath string) { - buf := new(bytes.Buffer) - command := cmd.NewRootCmd(buf, inputFS, "dot") - command.SetArgs([]string{"--filename", filepath.Base(dockerfilePath), "--output", "dot"}) - command.SetOut(buf) - command.SetErr(buf) + // Run the CLI in Docker to generate Dockerfile.dot + dockerCmd := exec.Command( + "docker", "run", "--rm", + "-u", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), + "-v", tempDir+":/data", + "-w", "/data", + "dockerfilegraph-test", + "--filename", "Dockerfile", "--output", "dot", + ) + dockerOut, err := dockerCmd.CombinedOutput() + if err != nil { + t.Fatalf("docker run CLI failed: %v\n%s", err, string(dockerOut)) + } - if err := command.Execute(); err != nil { - t.Fatalf("CLI execution for DOT failed: %v\nOutput: %s", err, buf.String()) + // Read the DOT file generated by the CLI + dotFile := filepath.Join(tempDir, "Dockerfile.dot") + outputBytes, err := os.ReadFile(dotFile) + if err != nil { + t.Fatalf("failed to read generated dot file: %v", err) } + + checkGoldenFile(t, outputBytes) } func checkGoldenFile(t *testing.T, dotBytes []byte) { diff --git a/internal/cmd/testdata/Dockerfile.golden.dot b/internal/cmd/testdata/Dockerfile.golden.dot index f1f49100..5ea2a1e7 100644 --- a/internal/cmd/testdata/Dockerfile.golden.dot +++ b/internal/cmd/testdata/Dockerfile.golden.dot @@ -1,5 +1,5 @@ digraph G { - graph [bb="0,0,504,252", + graph [bb="0,0,543,252", compound=true, nodesep=1.00, rankdir=LR, @@ -10,63 +10,63 @@ digraph G { fontcolor=grey20, height=0.5, label="ubuntu:l...887c2c7ac", - pos="72,234", + pos="86,234", shape=box, style="dashed,rounded", - width=2]; + width=2.3056]; stage_0 [height=0.5, label=ubuntu, - pos="252,234", + pos="285.5,234", shape=box, style=rounded, width=2]; - external_image_0 -> stage_0 [pos="e,179.61,234 144.37,234 152.26,234 160.34,234 168.36,234"]; + external_image_0 -> stage_0 [pos="e,213.41,234 169.04,234 180.32,234 191.9,234 203.17,234"]; stage_2 [fillcolor=grey90, height=0.5, label=release, - pos="432,126", + pos="471,126", shape=box, style="filled,rounded", width=2]; stage_0 -> stage_2 [arrowhead=empty, - pos="e,400.66,144.41 283.2,215.68 312.86,197.68 358.3,170.11 390.96,150.29", + pos="e,439.19,144.14 317.21,215.92 348.31,197.62 396.52,169.25 430.45,149.28", style=dashed]; external_image_1 [color=grey20, fontcolor=grey20, height=0.5, label="golang:1...b738433da", - pos="72,126", + pos="86,126", shape=box, style="dashed,rounded", - width=2]; + width=2.3889]; stage_1 [height=0.5, label="build-tool-depend...", - pos="252,126", + pos="285.5,126", shape=box, style=rounded, - width=2]; - external_image_1 -> stage_1 [pos="e,179.61,126 144.37,126 152.26,126 160.34,126 168.36,126"]; + width=2.1528]; + external_image_1 -> stage_1 [pos="e,207.99,126 172.19,126 180.65,126 189.25,126 197.73,126"]; stage_1 -> stage_2 [arrowhead=empty, - pos="e,359.61,126 324.37,126 332.26,126 340.34,126 348.36,126", + pos="e,398.82,126 363.26,126 371.64,126 380.19,126 388.62,126", style=dashed]; external_image_2 [color=grey20, fontcolor=grey20, height=0.5, label=buildcache, - pos="72,18", + pos="86,18", shape=box, style="dashed,rounded", width=2]; external_image_2 -> stage_1 [arrowhead=ediamond, - pos="e,220.66,107.59 103.2,36.321 132.48,54.086 177.12,81.177 209.69,100.93", + pos="e,251.34,107.86 120.06,36.077 153.1,54.144 204.09,82.03 240.53,101.96", style=dotted]; external_image_3 [color=grey20, fontcolor=grey20, height=0.5, label=scratch, - pos="252,18", + pos="285.5,18", shape=box, style="dashed,rounded", width=2]; - external_image_3 -> stage_2 [pos="e,400.66,107.59 283.2,36.321 312.86,54.319 358.3,81.888 390.96,101.71"]; + external_image_3 -> stage_2 [pos="e,439.19,107.86 317.21,36.077 348.31,54.378 396.52,82.753 430.45,102.72"]; } From 718ed27e28c9a03da44358cc8a3bf8980712809c Mon Sep 17 00:00:00 2001 From: Patrick Hoefler Date: Sun, 11 May 2025 02:19:01 +0200 Subject: [PATCH 7/7] refactor: enhance error messages in integration test for better clarity --- internal/cmd/integration_test.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go index 9f7caa0b..1240db35 100644 --- a/internal/cmd/integration_test.go +++ b/internal/cmd/integration_test.go @@ -21,16 +21,21 @@ func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { makeCmd.Dir = projectRoot makeOut, err := makeCmd.CombinedOutput() if err != nil { - t.Fatalf("make build-linux failed: %v\n%s", err, string(makeOut)) + t.Fatalf("make build-linux failed: %v\nOutput:\n%s", err, string(makeOut)) } - defer os.Remove(filepath.Join(projectRoot, "dockerfilegraph")) + binPath := filepath.Join(projectRoot, "dockerfilegraph") + defer func() { + if err := os.Remove(binPath); err != nil && !os.IsNotExist(err) { + t.Errorf("failed to remove built binary %s: %v", binPath, err) + } + }() // Build the Docker image from the project root buildCmd := exec.Command("docker", "build", "-t", "dockerfilegraph-test", "-f", "Dockerfile", ".") buildCmd.Dir = projectRoot buildOut, err := buildCmd.CombinedOutput() if err != nil { - t.Fatalf("docker build failed: %v\n%s", err, string(buildOut)) + t.Fatalf("docker build failed: %v\nOutput:\n%s", err, string(buildOut)) } // Prepare temp dir for output @@ -40,10 +45,10 @@ func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { dockerfileDst := filepath.Join(tempDir, "Dockerfile") content, err := os.ReadFile(dockerfileSrc) if err != nil { - t.Fatalf("failed to read example Dockerfile: %v", err) + t.Fatalf("failed to read example Dockerfile from %s: %v", dockerfileSrc, err) } if err := os.WriteFile(dockerfileDst, content, 0644); err != nil { - t.Fatalf("failed to write Dockerfile to temp dir: %v", err) + t.Fatalf("failed to write Dockerfile to temp dir %s: %v", dockerfileDst, err) } // Run the CLI in Docker to generate Dockerfile.dot @@ -57,14 +62,14 @@ func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { ) dockerOut, err := dockerCmd.CombinedOutput() if err != nil { - t.Fatalf("docker run CLI failed: %v\n%s", err, string(dockerOut)) + t.Fatalf("docker run CLI failed: %v\nOutput:\n%s", err, string(dockerOut)) } // Read the DOT file generated by the CLI dotFile := filepath.Join(tempDir, "Dockerfile.dot") outputBytes, err := os.ReadFile(dotFile) if err != nil { - t.Fatalf("failed to read generated dot file: %v", err) + t.Fatalf("failed to read generated dot file %s: %v", dotFile, err) } checkGoldenFile(t, outputBytes)