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
8 changes: 7 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@

## Testing Guidelines

- **TDD Approach**: Follow Red-Green-Refactor cycle for all new features
- **Red**: Write failing tests first that describe the desired behavior
- **Green**: Write minimal code to make tests pass
- **Refactor**: Clean up code while keeping all tests passing
- **No temporary test files**: Never create standalone test files like "test-*.dockerfile", "quick-test.*", etc.
- **Use existing test suites**: Add proper test cases to existing test files in the appropriate `*_test.go` files
- **Follow test patterns**: Use the established testing patterns in the codebase (table-driven tests, proper assertions)
- **Clean testing**: If testing is needed, extend the existing test suite rather than creating throwaway files
- **Focus on correct behavior**: Tests should verify expected functionality, not document bugs or regressions
- **Positive test naming**: Name tests to describe what they verify (e.g., `Test_separateScratchConnections`) rather than what's broken
- **Test naming**: Name tests to describe what they verify (e.g., `Test_separateScratchConnections`) rather than what's broken
- **Clear test comments**: Comments should explain what behavior is being tested, not reference historical issues
- **Comprehensive coverage**: Ensure edge cases, error conditions, and integration scenarios are tested

## Code Quality

- Follow the existing code style and patterns
- Use proper error handling
- Write comprehensive tests for new functionality
- Maintain backward compatibility unless explicitly breaking changes are needed
- Always use `make check` for efficient linting and testing - it runs golangci-lint, tests with coverage, and enforces code quality standards in one command
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Flags:
-n, --nodesep float minimum space between two adjacent nodes in the same rank (default 1)
-o, --output output file format, one of: canon, dot, pdf, png, raw, svg (default pdf)
-r, --ranksep float minimum separation between ranks (default 0.5)
--separate-scratch create separate nodes for each scratch image instead of collapsing them
--scratch how to handle scratch images, one of: collapsed, hidden, separated (default collapsed)
-u, --unflatten uint stagger length of leaf edges between [1,u] (default 0)
--version display the version of dockerfilegraph
```
Expand Down
41 changes: 22 additions & 19 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ import (
)

var (
concentrateFlag bool
dpiFlag uint
edgestyleFlag enum
filenameFlag string
layersFlag bool
legendFlag bool
maxLabelLengthFlag uint
nodesepFlag float64
outputFlag enum
ranksepFlag float64
separateScratchFlag bool
unflattenFlag uint
versionFlag bool
concentrateFlag bool
dpiFlag uint
edgestyleFlag enum
filenameFlag string
layersFlag bool
legendFlag bool
maxLabelLengthFlag uint
nodesepFlag float64
outputFlag enum
ranksepFlag float64
scratchFlag enum
unflattenFlag uint
versionFlag bool
)

// dfgWriter is a writer that prints to stdout. When testing, we
Expand Down Expand Up @@ -63,12 +63,15 @@ It creates a visual graph representation of the build process.`,
return
}

// Determine scratch mode from flag
scratchMode := scratchFlag.String()

// Load and parse the Dockerfile.
dockerfile, err := dockerfile2dot.LoadAndParseDockerfile(
inputFS,
filenameFlag,
int(maxLabelLengthFlag),
separateScratchFlag,
scratchMode,
)
if err != nil {
return
Expand Down Expand Up @@ -229,11 +232,11 @@ It creates a visual graph representation of the build process.`,
"minimum separation between ranks",
)

rootCmd.Flags().BoolVar(
&separateScratchFlag,
"separate-scratch",
false,
"create separate nodes for each scratch image instead of collapsing them",
scratchFlag = newEnum("collapsed", "separated", "hidden")
rootCmd.Flags().Var(
&scratchFlag,
"scratch",
"how to handle scratch images, one of: "+strings.Join(scratchFlag.AllowedValues(), ", "),
)

rootCmd.Flags().UintVarP(
Expand Down
80 changes: 49 additions & 31 deletions internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Flags:
-n, --nodesep float minimum space between two adjacent nodes in the same rank (default 1)
-o, --output output file format, one of: canon, dot, pdf, png, raw, svg (default pdf)
-r, --ranksep float minimum separation between ranks (default 0.5)
--separate-scratch create separate nodes for each scratch image instead of collapsing them
--scratch how to handle scratch images, one of: collapsed, hidden, separated (default collapsed)
-u, --unflatten uint stagger length of leaf edges between [1,u] (default 0)
--version display the version of dockerfilegraph
`
Expand Down Expand Up @@ -492,8 +492,29 @@ It creates a visual graph representation of the build process.
`,
},
{
name: "separate scratch flag",
cliArgs: []string{"--separate-scratch", "-o", "raw"},
name: "scratch flag collapsed mode",
cliArgs: []string{"--scratch", "collapsed", "-o", "raw"},
dockerfileContent: "FROM scratch AS app1\nCOPY app1.txt /app1.txt\n\n" +
"FROM scratch AS app2\nCOPY app2.txt /app2.txt\n",
wantOut: "Successfully created Dockerfile.raw\n",
wantOutFile: "Dockerfile.raw",
wantOutFileContent: `digraph G {
compound=true;
nodesep=1.00;
rankdir=LR;
ranksep=0.50;
external_image_0->stage_0;
external_image_0->stage_1;
external_image_0 [ color=grey20, fontcolor=grey20, label="scratch", shape=box, style="dashed,rounded", width=2 ];
stage_0 [ label="app1", shape=box, style=rounded, width=2 ];
stage_1 [ fillcolor=grey90, label="app2", shape=box, style="filled,rounded", width=2 ];

}
`,
},
{
name: "scratch flag separated mode",
cliArgs: []string{"--scratch", "separated", "-o", "raw"},
dockerfileContent: "FROM scratch AS app1\nCOPY app1.txt /app1.txt\n\n" +
"FROM scratch AS app2\nCOPY app2.txt /app2.txt\n",
wantOut: "Successfully created Dockerfile.raw\n",
Expand All @@ -513,6 +534,30 @@ It creates a visual graph representation of the build process.
}
`,
},
{
name: "scratch flag hidden mode",
cliArgs: []string{"--scratch", "hidden", "-o", "raw"},
dockerfileContent: "FROM scratch AS app1\nCOPY app1.txt /app1.txt\n\n" +
"FROM scratch AS app2\nCOPY app2.txt /app2.txt\n",
wantOut: "Successfully created Dockerfile.raw\n",
wantOutFile: "Dockerfile.raw",
wantOutFileContent: `digraph G {
compound=true;
nodesep=1.00;
rankdir=LR;
ranksep=0.50;
stage_0 [ label="app1", shape=box, style=rounded, width=2 ];
stage_1 [ fillcolor=grey90, label="app2", shape=box, style="filled,rounded", width=2 ];

}
`,
},
{
name: "scratch flag invalid mode",
cliArgs: []string{"--scratch", "invalid", "-o", "raw"},
wantErr: true,
wantOutRegex: `invalid argument "invalid" for "--scratch" flag: invalid value: invalid`,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -561,40 +606,13 @@ It creates a visual graph representation of the build process.
}
})

// Cleanup
// Cleanup output files written to real filesystem
if tt.wantOutFile != "" {
os.Remove(tt.wantOutFile)
}
}
}

func TestExecute(t *testing.T) {
tests := []test{
{
name: "should work",
wantOutFile: "Dockerfile.pdf",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_ = os.WriteFile("Dockerfile", []byte(dockerfileContent), 0644)

cmd.Execute()

if tt.wantOutFile != "" {
_, err := os.Stat(tt.wantOutFile)
if err != nil {
t.Errorf("%s: %v", tt.name, err)
}
}

// Cleanup
os.Remove("Dockerfile")
os.Remove(tt.wantOutFile)
})
}
}

func checkWantOut(t *testing.T, tt test, buf *bytes.Buffer) {
if tt.wantOut == "" && tt.wantOutRegex == "" {
t.Fatalf("Either wantOut or wantOutRegex must be set")
Expand Down
Loading
Loading