Skip to content

Commit 9d96db4

Browse files
Ivan GorshkovIvan Gorshkov
authored andcommitted
reverse strategy + multitarget tests
1 parent 8182ca6 commit 9d96db4

File tree

3 files changed

+480
-61
lines changed

3 files changed

+480
-61
lines changed
Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
package integration_tests
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/speakeasy-api/sdk-gen-config/workflow"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestMultiTargetCustomCode(t *testing.T) {
15+
t.Parallel()
16+
17+
// Build the speakeasy binary once for all subtests (using separate binary name)
18+
speakeasyBinary := buildSpeakeasyBinaryOnce(t, "speakeasy-customcode-multitarget-test-binary")
19+
20+
t.Run("BasicWorkflowMultiTarget", func(t *testing.T) {
21+
t.Parallel()
22+
testMultiTargetCustomCodeBasicWorkflow(t, speakeasyBinary)
23+
})
24+
25+
t.Run("AllTargetsModified", func(t *testing.T) {
26+
t.Parallel()
27+
testMultiTargetCustomCodeAllTargetsModified(t, speakeasyBinary)
28+
})
29+
30+
t.Run("IncrementalCustomCodeToOneTarget", func(t *testing.T) {
31+
t.Parallel()
32+
testMultiTargetIncrementalCustomCode(t, speakeasyBinary)
33+
})
34+
35+
t.Run("ConflictResolutionAcceptOurs", func(t *testing.T) {
36+
t.Parallel()
37+
testMultiTargetCustomCodeConflictResolutionAcceptOurs(t, speakeasyBinary)
38+
})
39+
}
40+
41+
// testMultiTargetCustomCodeBasicWorkflow tests basic custom code registration and reapplication
42+
// in a multi-target scenario (go, typescript)
43+
func testMultiTargetCustomCodeBasicWorkflow(t *testing.T, speakeasyBinary string) {
44+
temp := setupMultiTargetSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
45+
46+
// Path to go target file
47+
goFilePath := filepath.Join(temp, "go", "models", "operations", "getuserbyname.go")
48+
49+
// Step 1: Modify only the go target file
50+
modifyLineInFile(t, goFilePath, 10, "\t// custom code in go target")
51+
52+
// Step 2: Register custom code
53+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
54+
customCodeCmd.Dir = temp
55+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
56+
require.NoError(t, customCodeErr, "customcode command should succeed: %s", string(customCodeOutput))
57+
58+
// Step 3: Verify patch file was created only for go target
59+
goPatchFile := filepath.Join(temp, "go", ".speakeasy", "patches", "custom-code.diff")
60+
_, err := os.Stat(goPatchFile)
61+
require.NoError(t, err, "Go patch file should exist at %s", goPatchFile)
62+
63+
// Step 4: Verify patch file was NOT created for typescript
64+
tsPatchFile := filepath.Join(temp, "typescript", ".speakeasy", "patches", "custom-code.diff")
65+
_, err = os.Stat(tsPatchFile)
66+
require.True(t, os.IsNotExist(err), "TypeScript patch file should not exist")
67+
68+
// Step 5: Regenerate all targets
69+
runRegeneration(t, speakeasyBinary, temp, true)
70+
71+
// Step 6: Verify custom code is present in go target
72+
verifyCustomCodePresent(t, goFilePath, "// custom code in go target")
73+
74+
// Step 7: Verify typescript file doesn't have the custom code
75+
tsFilePath := filepath.Join(temp, "typescript", "src", "models", "operations", "getuserbyname.ts")
76+
if _, err := os.Stat(tsFilePath); err == nil {
77+
tsContent, err := os.ReadFile(tsFilePath)
78+
require.NoError(t, err, "Failed to read typescript file")
79+
require.NotContains(t, string(tsContent), "custom code in go target", "TypeScript file should not contain go custom code")
80+
}
81+
}
82+
83+
// testMultiTargetCustomCodeAllTargetsModified tests custom code registration and reapplication
84+
// when all targets (go, typescript) are modified
85+
func testMultiTargetCustomCodeAllTargetsModified(t *testing.T, speakeasyBinary string) {
86+
temp := setupMultiTargetSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
87+
88+
// Paths to all target files
89+
goFilePath := filepath.Join(temp, "go", "models", "operations", "getuserbyname.go")
90+
tsFilePath := filepath.Join(temp, "typescript", "src", "models", "operations", "getuserbyname.ts")
91+
92+
// Step 1: Modify all target files with target-specific custom code
93+
// Modify comment lines that are safe to change
94+
modifyLineInFile(t, goFilePath, 10, "\t// custom code in go target")
95+
modifyLineInFile(t, tsFilePath, 9, "// custom code in typescript target")
96+
97+
// Step 2: Register custom code
98+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
99+
customCodeCmd.Dir = temp
100+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
101+
require.NoError(t, customCodeErr, "customcode command should succeed: %s", string(customCodeOutput))
102+
103+
// Step 3: Verify patch files were created for all targets
104+
goPatchFile := filepath.Join(temp, "go", ".speakeasy", "patches", "custom-code.diff")
105+
_, err := os.Stat(goPatchFile)
106+
require.NoError(t, err, "Go patch file should exist")
107+
108+
tsPatchFile := filepath.Join(temp, "typescript", ".speakeasy", "patches", "custom-code.diff")
109+
_, err = os.Stat(tsPatchFile)
110+
require.NoError(t, err, "TypeScript patch file should exist")
111+
112+
// Step 4: Regenerate all targets
113+
runRegeneration(t, speakeasyBinary, temp, true)
114+
115+
// Step 5: Verify each target has its own custom code
116+
goContent, err := os.ReadFile(goFilePath)
117+
require.NoError(t, err, "Failed to read go file")
118+
require.Contains(t, string(goContent), "custom code in go target", "Go file should contain go custom code")
119+
120+
tsContent, err := os.ReadFile(tsFilePath)
121+
require.NoError(t, err, "Failed to read typescript file")
122+
require.Contains(t, string(tsContent), "custom code in typescript target", "TypeScript file should contain typescript custom code")
123+
124+
// Step 6: Verify no cross-contamination between targets
125+
require.NotContains(t, string(goContent), "custom code in typescript target", "Go file should not contain typescript custom code")
126+
require.NotContains(t, string(tsContent), "custom code in go target", "TypeScript file should not contain go custom code")
127+
}
128+
129+
// setupMultiTargetSDKGeneration sets up a test directory with multi-target SDK generation
130+
// and git initialization in the root
131+
func setupMultiTargetSDKGeneration(t *testing.T, speakeasyBinary, inputDoc string) string {
132+
t.Helper()
133+
134+
temp := setupCustomCodeTestDir(t)
135+
136+
// Create workflow file with multiple targets
137+
workflowFile := &workflow.Workflow{
138+
Version: workflow.WorkflowVersion,
139+
Sources: make(map[string]workflow.Source),
140+
Targets: make(map[string]workflow.Target),
141+
}
142+
143+
workflowFile.Sources["first-source"] = workflow.Source{
144+
Inputs: []workflow.Document{
145+
{
146+
Location: workflow.LocationString(inputDoc),
147+
},
148+
},
149+
}
150+
151+
// Setup two targets: go, typescript
152+
goOutput := "go"
153+
tsOutput := "typescript"
154+
155+
workflowFile.Targets["go-target"] = workflow.Target{
156+
Target: "go",
157+
Source: "first-source",
158+
Output: &goOutput,
159+
}
160+
161+
workflowFile.Targets["typescript-target"] = workflow.Target{
162+
Target: "typescript",
163+
Source: "first-source",
164+
Output: &tsOutput,
165+
}
166+
167+
if isLocalFileReference(inputDoc) {
168+
err := copyFile("resources/customcodespec.yaml", fmt.Sprintf("%s/%s", temp, inputDoc))
169+
require.NoError(t, err)
170+
}
171+
172+
err := os.MkdirAll(filepath.Join(temp, ".speakeasy"), 0o755)
173+
require.NoError(t, err)
174+
err = workflow.Save(temp, workflowFile)
175+
require.NoError(t, err)
176+
177+
// Run speakeasy run command to generate all targets
178+
runCmd := exec.Command(speakeasyBinary, "run", "-t", "all", "--pinned", "--skip-compile")
179+
runCmd.Dir = temp
180+
runOutput, runErr := runCmd.CombinedOutput()
181+
require.NoError(t, runErr, "speakeasy run should succeed: %s", string(runOutput))
182+
183+
// Verify both target directories were generated
184+
goDirInfo, err := os.Stat(filepath.Join(temp, "go"))
185+
require.NoError(t, err, "Go directory should exist")
186+
require.True(t, goDirInfo.IsDir(), "Go should be a directory")
187+
188+
tsDirInfo, err := os.Stat(filepath.Join(temp, "typescript"))
189+
require.NoError(t, err, "TypeScript directory should exist")
190+
require.True(t, tsDirInfo.IsDir(), "TypeScript should be a directory")
191+
192+
// Initialize git repository in the ROOT directory (not per target)
193+
initGitRepo(t, temp)
194+
195+
// Commit all generated files with "clean generation" message
196+
gitCommit(t, temp, "clean generation")
197+
198+
// Verify the commit was created with the correct message
199+
verifyGitCommit(t, temp, "clean generation")
200+
201+
return temp
202+
}
203+
204+
// testMultiTargetIncrementalCustomCode tests adding custom code to all targets,
205+
// then adding more custom code to only one target (go) and verifying all custom code is preserved
206+
func testMultiTargetIncrementalCustomCode(t *testing.T, speakeasyBinary string) {
207+
temp := setupMultiTargetSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
208+
209+
// Paths to all target files
210+
goFilePath := filepath.Join(temp, "go", "models", "operations", "getuserbyname.go")
211+
tsFilePath := filepath.Join(temp, "typescript", "src", "models", "operations", "getuserbyname.ts")
212+
213+
// Step 1: Add initial custom code to all targets
214+
modifyLineInFile(t, goFilePath, 10, "\t// initial custom code in go target")
215+
modifyLineInFile(t, tsFilePath, 9, "// initial custom code in typescript target")
216+
217+
// Step 2: Register custom code for all targets
218+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
219+
customCodeCmd.Dir = temp
220+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
221+
require.NoError(t, customCodeErr, "first customcode command should succeed: %s", string(customCodeOutput))
222+
223+
// Step 3: Verify patch files were created for all targets
224+
goPatchFile := filepath.Join(temp, "go", ".speakeasy", "patches", "custom-code.diff")
225+
_, err := os.Stat(goPatchFile)
226+
require.NoError(t, err, "Go patch file should exist")
227+
228+
tsPatchFile := filepath.Join(temp, "typescript", ".speakeasy", "patches", "custom-code.diff")
229+
_, err = os.Stat(tsPatchFile)
230+
require.NoError(t, err, "TypeScript patch file should exist")
231+
232+
// Step 4: Regenerate all targets
233+
runRegeneration(t, speakeasyBinary, temp, true)
234+
235+
// Step 5: Verify initial custom code is present in all targets
236+
goContent, err := os.ReadFile(goFilePath)
237+
require.NoError(t, err, "Failed to read go file")
238+
require.Contains(t, string(goContent), "initial custom code in go target", "Go file should contain initial custom code")
239+
240+
tsContent, err := os.ReadFile(tsFilePath)
241+
require.NoError(t, err, "Failed to read typescript file")
242+
require.Contains(t, string(tsContent), "initial custom code in typescript target", "TypeScript file should contain initial custom code")
243+
244+
// Commit the regenerated files
245+
gitCommit(t, temp, "regeneration with initial custom code")
246+
247+
// Step 6: Add MORE custom code to go target only (on a different line)
248+
modifyLineInFile(t, goFilePath, 8, "// additional custom code in go target")
249+
250+
// Step 7: Register the new custom code (should update go patch only)
251+
customCodeCmd2 := exec.Command(speakeasyBinary, "customcode", "--output", "console")
252+
customCodeCmd2.Dir = temp
253+
customCodeOutput2, customCodeErr2 := customCodeCmd2.CombinedOutput()
254+
require.NoError(t, customCodeErr2, "second customcode command should succeed: %s", string(customCodeOutput2))
255+
256+
// Step 8: Regenerate all targets again
257+
runRegeneration(t, speakeasyBinary, temp, true)
258+
259+
// Step 9: Verify go target has BOTH initial and additional custom code
260+
goContent, err = os.ReadFile(goFilePath)
261+
require.NoError(t, err, "Failed to read go file")
262+
require.Contains(t, string(goContent), "initial custom code in go target", "Go file should still contain initial custom code")
263+
require.Contains(t, string(goContent), "additional custom code in go target", "Go file should contain additional custom code")
264+
265+
// Step 10: Verify typescript still has its original custom code (unchanged)
266+
tsContent, err = os.ReadFile(tsFilePath)
267+
require.NoError(t, err, "Failed to read typescript file")
268+
require.Contains(t, string(tsContent), "initial custom code in typescript target", "TypeScript file should still contain its custom code")
269+
require.NotContains(t, string(tsContent), "additional custom code", "TypeScript file should not contain additional go custom code")
270+
}
271+
272+
// testMultiTargetCustomCodeConflictResolutionAcceptOurs tests conflict resolution in one target
273+
// while preserving custom code in other targets when accepting spec changes (ours)
274+
func testMultiTargetCustomCodeConflictResolutionAcceptOurs(t *testing.T, speakeasyBinary string) {
275+
temp := setupMultiTargetSDKGeneration(t, speakeasyBinary, "customcodespec.yaml")
276+
277+
// Paths to all target files
278+
goFilePath := filepath.Join(temp, "go", "models", "operations", "getuserbyname.go")
279+
tsFilePath := filepath.Join(temp, "typescript", "src", "models", "operations", "getuserbyname.ts")
280+
281+
// Step 1: Add custom code to ALL targets
282+
modifyLineInFile(t, goFilePath, 10, "\t// custom code in go target")
283+
modifyLineInFile(t, tsFilePath, 9, "// custom code in typescript target")
284+
285+
// Step 2: Register custom code for all targets
286+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
287+
customCodeCmd.Dir = temp
288+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
289+
require.NoError(t, customCodeErr, "customcode command should succeed: %s", string(customCodeOutput))
290+
291+
// Step 3: Verify patch files were created for both targets
292+
goPatchFile := filepath.Join(temp, "go", ".speakeasy", "patches", "custom-code.diff")
293+
_, err := os.Stat(goPatchFile)
294+
require.NoError(t, err, "Go patch file should exist")
295+
296+
tsPatchFile := filepath.Join(temp, "typescript", ".speakeasy", "patches", "custom-code.diff")
297+
_, err = os.Stat(tsPatchFile)
298+
require.NoError(t, err, "TypeScript patch file should exist")
299+
300+
// Step 4: Modify the spec to cause conflict in GO target only (line 477 affects GetUserByName)
301+
specPath := filepath.Join(temp, "customcodespec.yaml")
302+
modifyLineInFile(t, specPath, 477, " description: 'spec change'")
303+
304+
// Step 5: Run speakeasy run - should detect conflict in GO target only
305+
regenCmd := exec.Command(speakeasyBinary, "run", "-t", "all", "--pinned", "--skip-compile")
306+
regenCmd.Dir = temp
307+
regenOutput, regenErr := regenCmd.CombinedOutput()
308+
require.Error(t, regenErr, "speakeasy run should exit with error after detecting conflicts: %s", string(regenOutput))
309+
require.Contains(t, string(regenOutput), "CUSTOM CODE CONFLICTS DETECTED", "Output should show conflict detection banner")
310+
require.Contains(t, string(regenOutput), "Entering automatic conflict resolution mode", "Output should indicate automatic resolution mode")
311+
312+
// Step 6: Verify conflict markers present in GO file only
313+
goContentAfterConflict, err := os.ReadFile(goFilePath)
314+
require.NoError(t, err, "Failed to read go file after conflict")
315+
require.Contains(t, string(goContentAfterConflict), "<<<<<<<", "Go file should contain conflict markers")
316+
317+
// TypeScript file should NOT have conflict markers
318+
tsContentAfterConflict, err := os.ReadFile(tsFilePath)
319+
require.NoError(t, err, "Failed to read typescript file after conflict")
320+
require.NotContains(t, string(tsContentAfterConflict), "<<<<<<<", "TypeScript file should not contain conflict markers")
321+
require.Contains(t, string(tsContentAfterConflict), "custom code in typescript target", "TypeScript file should still have its custom code")
322+
323+
// Step 7: Resolve the go conflict by accepting spec changes (ours)
324+
checkoutCmd := exec.Command("git", "checkout", "--ours", goFilePath)
325+
checkoutCmd.Dir = temp
326+
checkoutOutput, checkoutErr := checkoutCmd.CombinedOutput()
327+
require.NoError(t, checkoutErr, "git checkout --ours should succeed: %s", string(checkoutOutput))
328+
329+
// Step 8: Verify conflict markers are gone in go file
330+
goContentAfterCheckout, err := os.ReadFile(goFilePath)
331+
require.NoError(t, err, "Failed to read go file after checkout")
332+
require.NotContains(t, string(goContentAfterCheckout), "<<<<<<<", "Go file should not contain conflict markers after checkout")
333+
334+
// Step 9: Stage the resolved go file
335+
gitAddCmd := exec.Command("git", "add", goFilePath)
336+
gitAddCmd.Dir = temp
337+
gitAddOutput, gitAddErr := gitAddCmd.CombinedOutput()
338+
require.NoError(t, gitAddErr, "git add should succeed: %s", string(gitAddOutput))
339+
340+
// Step 10: Run customcode command to register the resolution
341+
customCodeCmd2 := exec.Command(speakeasyBinary, "customcode", "--output", "console")
342+
customCodeCmd2.Dir = temp
343+
customCodeOutput2, customCodeErr2 := customCodeCmd2.CombinedOutput()
344+
require.NoError(t, customCodeErr2, "customcode command should succeed after conflict resolution: %s", string(customCodeOutput2))
345+
346+
// Step 11: Verify patch files status
347+
// Go patch file should be empty or removed
348+
goPatchContent, err := os.ReadFile(goPatchFile)
349+
if err == nil {
350+
require.Empty(t, goPatchContent, "Go patch file should be empty after accepting ours")
351+
}
352+
353+
// TypeScript patch file should still exist with its content
354+
tsPatchContent, err := os.ReadFile(tsPatchFile)
355+
require.NoError(t, err, "TypeScript patch file should still exist")
356+
require.NotEmpty(t, tsPatchContent, "TypeScript patch file should not be empty")
357+
require.Contains(t, string(tsPatchContent), "custom code in typescript target", "TypeScript patch should contain typescript custom code")
358+
359+
// Step 12: Verify gen.lock files
360+
// Go's gen.lock should NOT contain customCodeCommitHash
361+
goGenLockPath := filepath.Join(temp, "go", ".speakeasy", "gen.lock")
362+
goGenLockContent, err := os.ReadFile(goGenLockPath)
363+
require.NoError(t, err, "Failed to read go gen.lock")
364+
require.NotContains(t, string(goGenLockContent), "customCodeCommitHash", "Go gen.lock should not contain customCodeCommitHash after accepting ours")
365+
366+
// TypeScript's gen.lock should still contain customCodeCommitHash
367+
tsGenLockPath := filepath.Join(temp, "typescript", ".speakeasy", "gen.lock")
368+
tsGenLockContent, err := os.ReadFile(tsGenLockPath)
369+
require.NoError(t, err, "Failed to read typescript gen.lock")
370+
require.Contains(t, string(tsGenLockContent), "customCodeCommitHash", "TypeScript gen.lock should still contain customCodeCommitHash")
371+
372+
// Step 13: Run regeneration again
373+
runRegeneration(t, speakeasyBinary, temp, true)
374+
375+
// Step 14: Verify final state
376+
// Go file should contain spec change, should NOT contain custom code
377+
goContentFinal, err := os.ReadFile(goFilePath)
378+
require.NoError(t, err, "Failed to read go file after final regeneration")
379+
require.Contains(t, string(goContentFinal), "spec change", "Go file should contain spec change")
380+
require.NotContains(t, string(goContentFinal), "custom code in go target", "Go file should not contain custom code after accepting ours")
381+
382+
// TypeScript file should still contain its custom code
383+
tsContentFinal, err := os.ReadFile(tsFilePath)
384+
require.NoError(t, err, "Failed to read typescript file after final regeneration")
385+
require.Contains(t, string(tsContentFinal), "custom code in typescript target", "TypeScript file should still contain its custom code")
386+
}

0 commit comments

Comments
 (0)