diff --git a/.gitignore b/.gitignore index c6c6b87f..be235f58 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ dockerfilegraph # dockerfilegraph Dockerfile.* !/Dockerfile.alpine +!Dockerfile.golden.dot !examples/dockerfiles/Dockerfile.large diff --git a/internal/cmd/integration_test.go b/internal/cmd/integration_test.go new file mode 100644 index 00000000..1240db35 --- /dev/null +++ b/internal/cmd/integration_test.go @@ -0,0 +1,106 @@ +package cmd_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestIntegrationCLIGeneratesOutputFile(t *testing.T) { + // Find the project root directory + _, thisFile, _, _ := runtime.Caller(0) + projectRoot := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "../..")) + + // 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("make build-linux failed: %v\nOutput:\n%s", err, string(makeOut)) + } + 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\nOutput:\n%s", err, string(buildOut)) + } + + // 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 from %s: %v", dockerfileSrc, err) + } + if err := os.WriteFile(dockerfileDst, content, 0644); err != nil { + t.Fatalf("failed to write Dockerfile to temp dir %s: %v", dockerfileDst, err) + } + + // 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\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 %s: %v", dotFile, err) + } + + checkGoldenFile(t, outputBytes) +} + +func checkGoldenFile(t *testing.T, dotBytes []byte) { + _, thisFile, _, _ := runtime.Caller(0) + goldenDir := filepath.Join(filepath.Dir(thisFile), "testdata") + 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, dotBytes, 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) + } + 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"+ + "Diff (-want +got):\n%s", + goldenFile, diff, + ) + } + } +} diff --git a/internal/cmd/testdata/Dockerfile.golden.dot b/internal/cmd/testdata/Dockerfile.golden.dot new file mode 100644 index 00000000..5ea2a1e7 --- /dev/null +++ b/internal/cmd/testdata/Dockerfile.golden.dot @@ -0,0 +1,72 @@ +digraph G { + graph [bb="0,0,543,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="86,234", + shape=box, + style="dashed,rounded", + width=2.3056]; + stage_0 [height=0.5, + label=ubuntu, + pos="285.5,234", + shape=box, + style=rounded, + width=2]; + 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="471,126", + shape=box, + style="filled,rounded", + width=2]; + stage_0 -> stage_2 [arrowhead=empty, + 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="86,126", + shape=box, + style="dashed,rounded", + width=2.3889]; + stage_1 [height=0.5, + label="build-tool-depend...", + pos="285.5,126", + shape=box, + style=rounded, + 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,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="86,18", + shape=box, + style="dashed,rounded", + width=2]; + external_image_2 -> stage_1 [arrowhead=ediamond, + 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="285.5,18", + shape=box, + style="dashed,rounded", + width=2]; + 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"]; +}