Skip to content

Commit 6d4d01e

Browse files
authored
feat: add --build flag to diff command (#694)
Add --build flag to orchestrion diff command to build and diff in one step. This provides a quality of life improvement for orchestrion.yaml authors by eliminating the need for separate build and diff commands. Usage: orchestrion diff --build ./... Replaces: orchestrion go build -work ./... && orchestrion diff /work/dir Fixes #692 Signed-off-by: Kemal Akkoyun <[email protected]> --------- Signed-off-by: Kemal Akkoyun <[email protected]>
1 parent 89c03a2 commit 6d4d01e

File tree

2 files changed

+549
-24
lines changed

2 files changed

+549
-24
lines changed

internal/cmd/diff.go

Lines changed: 175 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ package cmd
77

88
import (
99
"fmt"
10-
10+
"io"
11+
"os"
12+
"slices"
13+
"strings"
14+
15+
"github.com/DataDog/orchestrion/internal/binpath"
16+
"github.com/DataDog/orchestrion/internal/goproxy"
17+
"github.com/DataDog/orchestrion/internal/pin"
1118
"github.com/DataDog/orchestrion/internal/report"
1219
"github.com/urfave/cli/v2"
1320
)
@@ -33,20 +40,26 @@ var (
3340
Usage: "Also print synthetic and tracer weaved packages",
3441
}
3542

43+
buildFlag = cli.BoolFlag{
44+
Name: "build",
45+
Usage: "Execute a build with -work before generating the diff. All remaining arguments after flags are passed to the build command.",
46+
}
47+
3648
Diff = &cli.Command{
3749
Name: "diff",
38-
Usage: "Generates a diff between a nominal and orchestrion-instrumented build using a go work directory that can be obtained running `orchestrion go build -work -a`. This is incompatible with coverage related flags.",
50+
Usage: "Generates a diff between a nominal and orchestrion-instrumented build. Use --build to execute a build first, or provide a work directory path obtained from `orchestrion go build -work -a`. This is incompatible with coverage related flags.",
3951
Args: true,
4052
Flags: []cli.Flag{
4153
&filenameFlag,
4254
&filterFlag,
4355
&packageFlag,
4456
&debugFlag,
57+
&buildFlag,
4558
},
46-
Action: func(clictx *cli.Context) (err error) {
47-
workFolder := clictx.Args().First()
48-
if workFolder == "" {
49-
return cli.ShowSubcommandHelp(clictx)
59+
Action: func(clictx *cli.Context) error {
60+
workFolder, err := getWorkFolder(clictx)
61+
if err != nil {
62+
return err
5063
}
5164

5265
report, err := report.FromWorkDir(clictx.Context, workFolder)
@@ -69,25 +82,163 @@ var (
6982
}
7083
}
7184

72-
if clictx.Bool(packageFlag.Name) {
73-
for _, pkg := range report.Packages() {
74-
_, _ = fmt.Fprintln(clictx.App.Writer, pkg)
75-
}
76-
return nil
77-
}
85+
return outputReport(clictx, report)
86+
},
87+
}
88+
)
7889

79-
if clictx.Bool(filenameFlag.Name) {
80-
for _, file := range report.Files() {
81-
_, _ = fmt.Fprintln(clictx.App.Writer, file)
82-
}
83-
return nil
84-
}
90+
func getWorkFolder(clictx *cli.Context) (string, error) {
91+
if !clictx.Bool(buildFlag.Name) {
92+
workFolder := clictx.Args().First()
93+
if workFolder == "" {
94+
return "", cli.ShowSubcommandHelp(clictx)
95+
}
96+
return workFolder, nil
97+
}
8598

86-
if err := report.Diff(clictx.App.Writer); err != nil {
87-
return cli.Exit(fmt.Sprintf("failed to generate diff: %s", err), 1)
88-
}
99+
return executeBuildAndCaptureWorkDir(clictx, prepareBuildArgs(clictx.Args().Slice()))
100+
}
89101

90-
return nil
91-
},
102+
func prepareBuildArgs(args []string) []string {
103+
switch {
104+
case len(args) == 0:
105+
args = []string{"build", "./..."}
106+
case args[0] != "build" && args[0] != "install" && args[0] != "test":
107+
args = append([]string{"build"}, args...)
92108
}
93-
)
109+
110+
var flags []string
111+
hasWork, hasAll := false, false
112+
for _, arg := range args {
113+
switch arg {
114+
case "-work":
115+
hasWork = true
116+
case "-a":
117+
hasAll = true
118+
}
119+
}
120+
121+
if !hasWork {
122+
flags = append(flags, "-work")
123+
}
124+
if !hasAll {
125+
flags = append(flags, "-a")
126+
}
127+
128+
if len(flags) > 0 {
129+
args = slices.Concat(args[:1], flags, args[1:])
130+
}
131+
132+
return args
133+
}
134+
135+
func outputReport(clictx *cli.Context, rpt report.Report) error {
136+
if clictx.Bool(packageFlag.Name) {
137+
for _, pkg := range rpt.Packages() {
138+
_, _ = fmt.Fprintln(clictx.App.Writer, pkg)
139+
}
140+
return nil
141+
}
142+
143+
if clictx.Bool(filenameFlag.Name) {
144+
for _, file := range rpt.Files() {
145+
_, _ = fmt.Fprintln(clictx.App.Writer, file)
146+
}
147+
return nil
148+
}
149+
150+
if err := rpt.Diff(clictx.App.Writer); err != nil {
151+
return cli.Exit(fmt.Sprintf("failed to generate diff: %s", err), 1)
152+
}
153+
154+
return nil
155+
}
156+
157+
func executeBuildAndCaptureWorkDir(clictx *cli.Context, buildArgs []string) (string, error) {
158+
if err := pin.AutoPinOrchestrion(clictx.Context, clictx.App.Writer, clictx.App.ErrWriter); err != nil {
159+
return "", cli.Exit(err, -1)
160+
}
161+
162+
tmpFile, err := os.CreateTemp("", "orchestrion-build-output-")
163+
if err != nil {
164+
return "", fmt.Errorf("creating temporary file for build output: %w", err)
165+
}
166+
defer tmpFile.Close()
167+
defer os.Remove(tmpFile.Name())
168+
169+
originalStderr := os.Stderr
170+
os.Stderr = tmpFile
171+
172+
buildErr := goproxy.Run(clictx.Context, buildArgs, goproxy.WithToolexec(binpath.Orchestrion, "toolexec"))
173+
174+
os.Stderr = originalStderr
175+
176+
if buildErr != nil {
177+
return "", cli.Exit(fmt.Sprintf("build failed: %v", buildErr), 1)
178+
}
179+
180+
output, err := os.ReadFile(tmpFile.Name())
181+
if err != nil {
182+
return "", fmt.Errorf("reading build output: %w", err)
183+
}
184+
185+
wd := extractWorkDirFromOutput(string(output))
186+
if wd == "" {
187+
return "", cli.Exit("could not extract work directory from build output (did the build use -work?)", 1)
188+
}
189+
return wd, nil
190+
}
191+
192+
func extractWorkDirFromOutput(output string) string {
193+
for line := range strings.SplitSeq(output, "\n") {
194+
if wd, ok := strings.CutPrefix(strings.TrimSpace(line), "WORK="); ok {
195+
return wd
196+
}
197+
}
198+
return ""
199+
}
200+
201+
// ReportInterface defines the interface for report objects used in testing
202+
type ReportInterface interface {
203+
IsEmpty() bool
204+
WithSpecialCasesFilter() ReportInterface
205+
WithRegexFilter(pattern string) (ReportInterface, error)
206+
Packages() []string
207+
Files() []string
208+
Diff(io.Writer) error
209+
}
210+
211+
// Test helper functions for testing internal functionality
212+
213+
// PrepareBuildArgsForTest exposes prepareBuildArgs for testing
214+
func PrepareBuildArgsForTest(args []string) []string {
215+
return prepareBuildArgs(args)
216+
}
217+
218+
// ExtractWorkDirFromOutputForTest exposes extractWorkDirFromOutput for testing
219+
func ExtractWorkDirFromOutputForTest(output string) string {
220+
return extractWorkDirFromOutput(output)
221+
}
222+
223+
// OutputReportForTest exposes outputReport functionality for testing
224+
func OutputReportForTest(writer io.Writer, flags map[string]bool, rpt ReportInterface) error {
225+
if flags["package"] {
226+
for _, pkg := range rpt.Packages() {
227+
_, _ = fmt.Fprintln(writer, pkg)
228+
}
229+
return nil
230+
}
231+
232+
if flags["files"] {
233+
for _, file := range rpt.Files() {
234+
_, _ = fmt.Fprintln(writer, file)
235+
}
236+
return nil
237+
}
238+
239+
if err := rpt.Diff(writer); err != nil {
240+
return cli.Exit(fmt.Sprintf("failed to generate diff: %s", err), 1)
241+
}
242+
243+
return nil
244+
}

0 commit comments

Comments
 (0)