Skip to content

Commit 159d06c

Browse files
Ivan GorshkovIvan Gorshkov
authored andcommitted
Integration test for perfect custom code scenario
1 parent 25f4dd1 commit 159d06c

File tree

2 files changed

+1061
-0
lines changed

2 files changed

+1061
-0
lines changed

integration/customcode_test.go

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
package integration_tests
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
11+
"testing"
12+
13+
"github.com/speakeasy-api/sdk-gen-config/workflow"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestCustomCodeWorkflows(t *testing.T) {
18+
t.Parallel()
19+
20+
// Build the speakeasy binary once for all tests
21+
speakeasyBinary := buildSpeakeasyBinary(t)
22+
23+
tests := []struct {
24+
name string
25+
targetTypes []string
26+
inputDoc string
27+
withCodeSamples bool
28+
}{
29+
{
30+
name: "generation with local document",
31+
targetTypes: []string{
32+
"go",
33+
},
34+
inputDoc: "customcodespec.yaml",
35+
},
36+
}
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
t.Parallel()
40+
// Use custom-code-test directory to avoid gitignore issues from speakeasy repo
41+
temp := setupCustomCodeTestDir(t)
42+
43+
// Create workflow file and associated resources
44+
workflowFile := &workflow.Workflow{
45+
Version: workflow.WorkflowVersion,
46+
Sources: make(map[string]workflow.Source),
47+
Targets: make(map[string]workflow.Target),
48+
}
49+
workflowFile.Sources["first-source"] = workflow.Source{
50+
Inputs: []workflow.Document{
51+
{
52+
Location: workflow.LocationString(tt.inputDoc),
53+
},
54+
},
55+
}
56+
57+
for i := range tt.targetTypes {
58+
outdir := "go"
59+
target := workflow.Target{
60+
Target: tt.targetTypes[i],
61+
Source: "first-source",
62+
Output: &outdir,
63+
}
64+
if tt.withCodeSamples {
65+
target.CodeSamples = &workflow.CodeSamples{
66+
Output: "codeSamples.yaml",
67+
}
68+
}
69+
workflowFile.Targets[fmt.Sprintf("%d-target", i)] = target
70+
}
71+
72+
if isLocalFileReference(tt.inputDoc) {
73+
err := copyFile("resources/customcodespec.yaml", fmt.Sprintf("%s/%s", temp, tt.inputDoc))
74+
require.NoError(t, err)
75+
}
76+
77+
err := os.MkdirAll(filepath.Join(temp, ".speakeasy"), 0o755)
78+
require.NoError(t, err)
79+
err = workflow.Save(temp, workflowFile)
80+
require.NoError(t, err)
81+
82+
// Run speakeasy run command using the built binary
83+
runCmd := exec.Command(speakeasyBinary, "run", "-t", "all", "--pinned", "--skip-compile")
84+
runCmd.Dir = temp
85+
runOutput, runErr := runCmd.CombinedOutput()
86+
require.NoError(t, runErr, "speakeasy run should succeed: %s", string(runOutput))
87+
88+
// SDK directory where files are generated
89+
sdkDir := filepath.Join(temp, "go")
90+
91+
// Run go mod tidy to ensure go.sum is properly populated
92+
// This is necessary because we used --skip-compile above
93+
goModTidyCmd := exec.Command("go", "mod", "tidy")
94+
goModTidyCmd.Dir = sdkDir
95+
output, err := goModTidyCmd.CombinedOutput()
96+
require.NoError(t, err, "Failed to run go mod tidy: %s", string(output))
97+
98+
if tt.withCodeSamples {
99+
codeSamplesPath := filepath.Join(sdkDir, "codeSamples.yaml")
100+
content, err := os.ReadFile(codeSamplesPath)
101+
require.NoError(t, err, "No readable file %s exists", codeSamplesPath)
102+
103+
// Check if codeSamples file is not empty and contains expected content
104+
require.NotEmpty(t, content, "codeSamples.yaml should not be empty")
105+
}
106+
107+
// SDK is generated in go subdirectory
108+
for _, targetType := range tt.targetTypes {
109+
checkForExpectedFiles(t, sdkDir, expectedFilesByLanguage(targetType))
110+
}
111+
112+
// Initialize git repository in the go directory
113+
initGitRepo(t, sdkDir)
114+
115+
// Copy workflow.yaml and spec to SDK directory before committing
116+
copyWorkflowToSDK(t, temp, sdkDir)
117+
118+
// Commit all generated files with "clean generation" message
119+
gitCommit(t, sdkDir, "clean generation")
120+
121+
// Verify the commit was created with the correct message
122+
verifyGitCommit(t, sdkDir, "clean generation")
123+
124+
// Modify httpmetadata.go to add custom code
125+
httpMetadataPath := filepath.Join(sdkDir, "models", "components", "httpmetadata.go")
126+
modifyLineInFile(t, httpMetadataPath, 10, "\t// custom code")
127+
128+
// Run customcode command from SDK directory using the built binary
129+
customCodeCmd := exec.Command(speakeasyBinary, "customcode", "--output", "console")
130+
customCodeCmd.Dir = sdkDir
131+
customCodeOutput, customCodeErr := customCodeCmd.CombinedOutput()
132+
require.NoError(t, customCodeErr, "customcode command should succeed: %s", string(customCodeOutput))
133+
134+
// Verify patches directory was created in SDK directory
135+
patchesDir := filepath.Join(sdkDir, ".speakeasy", "patches")
136+
_, err = os.Stat(patchesDir)
137+
require.NoError(t, err, "patches directory should exist at %s", patchesDir)
138+
139+
// Verify patch file was created
140+
patchFile := filepath.Join(patchesDir, "custom-code.diff")
141+
_, err = os.Stat(patchFile)
142+
require.NoError(t, err, "patch file should exist at %s", patchFile)
143+
144+
// Run speakeasy run again from the SDK directory to regenerate and apply patches
145+
regenCmd := exec.Command(speakeasyBinary, "run", "-t", "all", "--pinned", "--skip-compile")
146+
regenCmd.Dir = sdkDir
147+
regenOutput, regenErr := regenCmd.CombinedOutput()
148+
require.NoError(t, regenErr, "speakeasy run should succeed on regeneration: %s", string(regenOutput))
149+
150+
// Verify the custom code is still present after regeneration
151+
httpMetadataContent, err := os.ReadFile(httpMetadataPath)
152+
require.NoError(t, err, "Failed to read httpmetadata.go after regeneration")
153+
require.Contains(t, string(httpMetadataContent), "// custom code", "Custom code comment should still be present after regeneration")
154+
})
155+
}
156+
}
157+
158+
// initGitRepo initializes a git repository in the specified directory
159+
func initGitRepo(t *testing.T, dir string) {
160+
t.Helper()
161+
162+
// Initialize git repo
163+
cmd := exec.Command("git", "init")
164+
cmd.Dir = dir
165+
output, err := cmd.CombinedOutput()
166+
require.NoError(t, err, "Failed to initialize git repo: %s", string(output))
167+
168+
// Configure git user for commits
169+
cmd = exec.Command("git", "config", "user.email", "test@example.com")
170+
cmd.Dir = dir
171+
output, err = cmd.CombinedOutput()
172+
require.NoError(t, err, "Failed to configure git user.email: %s", string(output))
173+
174+
cmd = exec.Command("git", "config", "user.name", "Test User")
175+
cmd.Dir = dir
176+
output, err = cmd.CombinedOutput()
177+
require.NoError(t, err, "Failed to configure git user.name: %s", string(output))
178+
}
179+
180+
// gitCommit creates a git commit with all changes in the specified directory
181+
func gitCommit(t *testing.T, dir, message string) {
182+
t.Helper()
183+
184+
// Add all files
185+
cmd := exec.Command("git", "add", ".")
186+
cmd.Dir = dir
187+
output, err := cmd.CombinedOutput()
188+
require.NoError(t, err, "Failed to git add: %s", string(output))
189+
190+
// Commit with message
191+
cmd = exec.Command("git", "commit", "-m", message)
192+
cmd.Dir = dir
193+
output, err = cmd.CombinedOutput()
194+
require.NoError(t, err, "Failed to git commit: %s", string(output))
195+
}
196+
197+
// verifyGitCommit verifies that a git commit exists with the expected message
198+
func verifyGitCommit(t *testing.T, dir, expectedMessage string) {
199+
t.Helper()
200+
201+
// Check that .git directory exists
202+
gitDir := filepath.Join(dir, ".git")
203+
_, err := os.Stat(gitDir)
204+
require.NoError(t, err, ".git directory should exist")
205+
206+
// Get the latest commit message
207+
cmd := exec.Command("git", "log", "-1", "--pretty=%B")
208+
cmd.Dir = dir
209+
output, err := cmd.CombinedOutput()
210+
require.NoError(t, err, "Failed to get git log: %s", string(output))
211+
212+
// Verify commit message matches
213+
commitMessage := strings.TrimSpace(string(output))
214+
require.Equal(t, expectedMessage, commitMessage, "Commit message should match expected message")
215+
}
216+
217+
// modifyLineInFile modifies a specific line in a file (1-indexed line number)
218+
func modifyLineInFile(t *testing.T, filePath string, lineNumber int, newContent string) {
219+
t.Helper()
220+
221+
// Read the file
222+
file, err := os.Open(filePath)
223+
require.NoError(t, err, "Failed to open file: %s", filePath)
224+
defer file.Close()
225+
226+
var lines []string
227+
scanner := bufio.NewScanner(file)
228+
for scanner.Scan() {
229+
lines = append(lines, scanner.Text())
230+
}
231+
require.NoError(t, scanner.Err(), "Failed to read file: %s", filePath)
232+
233+
// Modify the specific line (convert 1-indexed to 0-indexed)
234+
require.Less(t, lineNumber, len(lines)+1, "Line number %d is out of range (file has %d lines)", lineNumber, len(lines))
235+
lines[lineNumber-1] = newContent
236+
237+
// Write back to the file
238+
file, err = os.Create(filePath)
239+
require.NoError(t, err, "Failed to open file for writing: %s", filePath)
240+
defer file.Close()
241+
242+
writer := bufio.NewWriter(file)
243+
for _, line := range lines {
244+
_, err := writer.WriteString(line + "\n")
245+
require.NoError(t, err, "Failed to write line to file")
246+
}
247+
require.NoError(t, writer.Flush(), "Failed to flush writer")
248+
}
249+
250+
// buildSpeakeasyBinary builds the speakeasy binary and returns the path to it
251+
func buildSpeakeasyBinary(t *testing.T) string {
252+
t.Helper()
253+
254+
_, filename, _, _ := runtime.Caller(0)
255+
baseFolder := filepath.Join(filepath.Dir(filename), "..")
256+
binaryPath := filepath.Join(baseFolder, "speakeasy-test-binary")
257+
258+
// Build the binary
259+
cmd := exec.Command("go", "build", "-o", binaryPath, "./main.go")
260+
cmd.Dir = baseFolder
261+
output, err := cmd.CombinedOutput()
262+
require.NoError(t, err, "Failed to build speakeasy binary: %s", string(output))
263+
264+
// Clean up the binary when test completes
265+
t.Cleanup(func() {
266+
os.Remove(binaryPath)
267+
})
268+
269+
return binaryPath
270+
}
271+
272+
// copyWorkflowToSDK copies the workflow.yaml and spec files from the workspace to the SDK directory
273+
func copyWorkflowToSDK(t *testing.T, workspaceDir, sdkDir string) {
274+
t.Helper()
275+
276+
// Copy workflow.yaml file, removing output paths since we're running from SDK directory
277+
srcWorkflowPath := filepath.Join(workspaceDir, ".speakeasy", "workflow.yaml")
278+
dstWorkflowPath := filepath.Join(sdkDir, ".speakeasy", "workflow.yaml")
279+
workflowContent, err := os.ReadFile(srcWorkflowPath)
280+
require.NoError(t, err, "Failed to read workflow.yaml")
281+
282+
// Remove "output: go" lines from the workflow content
283+
workflowStr := string(workflowContent)
284+
workflowStr = strings.ReplaceAll(workflowStr, "\n output: go", "")
285+
286+
err = os.WriteFile(dstWorkflowPath, []byte(workflowStr), 0o644)
287+
require.NoError(t, err, "Failed to write workflow.yaml")
288+
289+
// Read the workflow to find spec files to copy
290+
workflowFile, _, err := workflow.Load(workspaceDir)
291+
require.NoError(t, err, "Failed to load workflow.yaml")
292+
293+
// Copy local spec files to SDK directory
294+
for _, source := range workflowFile.Sources {
295+
for i := range source.Inputs {
296+
if isLocalFileReference(string(source.Inputs[i].Location)) {
297+
specPath := string(source.Inputs[i].Location)
298+
299+
// Copy the spec file to SDK directory
300+
srcSpecPath := filepath.Join(workspaceDir, specPath)
301+
dstSpecPath := filepath.Join(sdkDir, specPath)
302+
303+
specContent, err := os.ReadFile(srcSpecPath)
304+
require.NoError(t, err, "Failed to read spec file: %s", srcSpecPath)
305+
306+
err = os.WriteFile(dstSpecPath, specContent, 0o644)
307+
require.NoError(t, err, "Failed to write spec file to SDK directory: %s", dstSpecPath)
308+
}
309+
}
310+
}
311+
}
312+
313+
// setupCustomCodeTestDir creates a test directory in custom-code-test/speakeasy_tests
314+
func setupCustomCodeTestDir(t *testing.T) string {
315+
t.Helper()
316+
317+
baseDir := "/Users/ivangorshkov/speakeasy/repos/custom-code-test/speakeasy_tests"
318+
319+
// Create base directory if it doesn't exist
320+
err := os.MkdirAll(baseDir, 0o755)
321+
require.NoError(t, err, "Failed to create base directory")
322+
323+
// Create unique test directory
324+
testDir := filepath.Join(baseDir, fmt.Sprintf("test-%d", os.Getpid()))
325+
err = os.MkdirAll(testDir, 0o755)
326+
require.NoError(t, err, "Failed to create test directory")
327+
328+
// Clean up after test
329+
t.Cleanup(func() {
330+
os.RemoveAll(testDir)
331+
})
332+
333+
return testDir
334+
}

0 commit comments

Comments
 (0)