Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Mocks for Windows #877

Merged
merged 23 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
101 changes: 84 additions & 17 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ jobs:
- name: Build
run: echo "Building on ${{ matrix.os }}"

- name: Add GNU tar to PATH (significantly faster than windows tar)
if: matrix.target == 'windows'
run: echo "C:\Program Files\Git\usr\bin" >> $Env:GITHUB_PATH

osterman marked this conversation as resolved.
Show resolved Hide resolved
- name: Check out code into the Go module directory
uses: actions/checkout@v4

Expand Down Expand Up @@ -75,19 +79,52 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: linux
- os: windows-latest
target: windows
- os: macos-latest
target: macos
flavor:
- { os: ubuntu-latest, target: linux }
- { os: windows-latest, target: windows }
- { os: macos-latest, target: macos }
timeout-minutes: 15
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.flavor.os }}
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4

- name: Add GNU tar to PATH (significantly faster than windows tar)
if: matrix.flavor.target == 'windows'
run: echo "C:\Program Files\Git\usr\bin" >> $Env:GITHUB_PATH

- name: Download build artifacts for ${{ matrix.flavor.target }}
uses: actions/download-artifact@v4
with:
name: build-artifacts-${{ matrix.flavor.target }}
path: ${{ github.workspace }}

- name: Add build artifacts directory to PATH for linux or macos
if: matrix.flavor.target == 'linux' || matrix.flavor.target == 'macos'
run: |
echo "${{ github.workspace }}" >> $GITHUB_PATH
chmod +x "${{ github.workspace }}/atmos"

- name: Add build artifacts directory to PATH for windows
if: matrix.flavor.target == 'windows'
run: |
echo "${{ github.workspace }}" >> $Env:GITHUB_PATH
osterman marked this conversation as resolved.
Show resolved Hide resolved

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
terraform_wrapper: false

- name: Check atmos.exe integrity
if: matrix.flavor.target == 'windows'
shell: pwsh
run: |
Write-Output "PATH=$Env:PATH"
Write-Output "PATHEXT=$Env:PATHEXT"
Get-ChildItem "${{ github.workspace }}"
Get-Command "${{ github.workspace }}\atmos.exe"
atmos version

- name: Set up Go
uses: actions/setup-go@v5
with:
Expand Down Expand Up @@ -279,26 +316,56 @@ jobs:

timeout-minutes: 20
steps:
- name: Download build artifacts
- name: Check out code into the Go module directory
uses: actions/checkout@v4

- name: Add GNU tar to PATH (significantly faster than windows tar)
if: matrix.flavor.target == 'windows'
run: echo "C:\Program Files\Git\usr\bin" >> $Env:GITHUB_PATH

- name: Download build artifacts for ${{ matrix.flavor.target }}
uses: actions/download-artifact@v4
with:
name: build-artifacts-${{ matrix.flavor.target }}
path: /usr/local/bin
path: ${{ github.workspace }}

- name: Set execute permissions on atmos
run: chmod +x /usr/local/bin/atmos
- name: Add build artifacts directory to PATH for linux or macos
if: matrix.flavor.target == 'linux' || matrix.flavor.target == 'macos'
run: |
echo "${{ github.workspace }}" >> $GITHUB_PATH
chmod +x "${{ github.workspace }}/atmos"

- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Add build artifacts directory to PATH for windows
if: matrix.flavor.target == 'windows'
run: |
echo "${{ github.workspace }}" >> $Env:GITHUB_PATH

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
terraform_wrapper: false

- name: Run tests for ${{ matrix.demo-folder }}
- name: Run tests in ${{ matrix.demo-folder }} for ${{ matrix.flavor.target }}
working-directory: examples/${{ matrix.demo-folder }}
if: matrix.flavor.target == 'linux' || matrix.flavor.target == 'macos'
run: |
atmos test

- name: Check atmos.exe integrity
if: matrix.flavor.target == 'windows'
shell: pwsh
run: |
Write-Output "PATH=$Env:PATH"
Write-Output "PATHEXT=$Env:PATHEXT"
Get-ChildItem "${{ github.workspace }}"
Get-Command "${{ github.workspace }}\atmos.exe"
atmos version

- name: Run tests in ${{ matrix.demo-folder }} for ${{ matrix.flavor.target }}
working-directory: examples/${{ matrix.demo-folder }}
if: matrix.flavor.target == 'windows'
shell: pwsh
run: |
cd examples/${{ matrix.demo-folder }}
atmos test

# run other demo tests
Expand Down Expand Up @@ -352,7 +419,7 @@ jobs:
--minimum-failure-severity=warning
--recursive
--config=${{ github.workspace }}/examples/.tflint.hcl
fail_on_error: true
fail_level: error
osterman marked this conversation as resolved.
Show resolved Hide resolved

# run other demo tests
validate:
Expand Down
250 changes: 250 additions & 0 deletions tests/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package tests

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update deprecated package usage, warrior!

The io/ioutil package is deprecated since Go 1.19. Let's use the os package instead.

-import (
-	"io/ioutil"
+import (
+	"os"

Then update the ReadFile calls:

-	data, err := ioutil.ReadFile(filePath)
+	data, err := os.ReadFile(filePath)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 golangci-lint (1.62.2)

7-7: SA1019: "io/ioutil" has been deprecated since Go 1.19: As of Go 1.16, the same functionality is now provided by package [io] or package [os], and those implementations should be preferred in new code. See the specific function documentation for details.

(staticcheck)

"os"
"os/exec"
"path/filepath" // For resolving absolute paths
"regexp"
"strings"
"testing"

"gopkg.in/yaml.v3"
)

type Expectation struct {
Stdout []string `yaml:"stdout"`
Stderr []string `yaml:"stderr"`
ExitCode int `yaml:"exit_code"`
FileExists []string `yaml:"file_exists"`
FileContains map[string][]string `yaml:"file_contains"`
}

type TestCase struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Enabled bool `yaml:"enabled"`
Workdir string `yaml:"workdir"`
Command string `yaml:"command"`
Args []string `yaml:"args"`
Env map[string]string `yaml:"env"`
Expect Expectation `yaml:"expect"`
}

type TestSuite struct {
Tests []TestCase `yaml:"tests"`
}

func loadTestSuite(filePath string) (*TestSuite, error) {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, err
}

var suite TestSuite
err = yaml.Unmarshal(data, &suite)
if err != nil {
return nil, err
}

return &suite, nil
}

type PathManager struct {
OriginalPath string
Prepended []string
}

// NewPathManager initializes a PathManager with the current PATH.
func NewPathManager() *PathManager {
return &PathManager{
OriginalPath: os.Getenv("PATH"),
Prepended: []string{},
}
}

// Prepend adds directories to the PATH with precedence.
func (pm *PathManager) Prepend(dirs ...string) {
for _, dir := range dirs {
absPath, err := filepath.Abs(dir)
if err != nil {
fmt.Printf("Failed to resolve absolute path for %q: %v\n", dir, err)
continue
}
pm.Prepended = append(pm.Prepended, absPath)
}
}

// GetPath returns the updated PATH.
func (pm *PathManager) GetPath() string {
return fmt.Sprintf("%s%c%s",
strings.Join(pm.Prepended, string(os.PathListSeparator)),
os.PathListSeparator,
pm.OriginalPath,
)
}

// Apply updates the PATH environment variable globally.
func (pm *PathManager) Apply() error {
return os.Setenv("PATH", pm.GetPath())
}

func TestCLICommands(t *testing.T) {
// Capture the starting working directory
startingDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get the current working directory: %v", err)
}

// Initialize PathManager and update PATH
pathManager := NewPathManager()
pathManager.Prepend("../build", "..")
err = pathManager.Apply()
if err != nil {
t.Fatalf("Failed to apply updated PATH: %v", err)
}
fmt.Printf("Updated PATH: %s\n", pathManager.GetPath())

testSuite, err := loadTestSuite("test_cases.yaml")
if err != nil {
t.Fatalf("Failed to load test suite: %v", err)
}

for _, tc := range testSuite.Tests {

if !tc.Enabled {
t.Logf("Skipping disabled test: %s", tc.Name)
continue
}

t.Run(tc.Name, func(t *testing.T) {
defer func() {
// Change back to the original working directory after the test
if err := os.Chdir(startingDir); err != nil {
t.Fatalf("Failed to change back to the starting directory: %v", err)
}
}()

// Change to the specified working directory
if tc.Workdir != "" {
err := os.Chdir(tc.Workdir)
if err != nil {
t.Fatalf("Failed to change directory to %q: %v", tc.Workdir, err)
}
}

// Check if the binary exists
binaryPath, err := exec.LookPath(tc.Command)
if err != nil {
t.Fatalf("Binary not found: %s. Current PATH: %s", tc.Command, pathManager.GetPath())
}

// Prepare the command
cmd := exec.Command(binaryPath, tc.Args...)

// Set environment variables
envVars := os.Environ()
for key, value := range tc.Env {
envVars = append(envVars, fmt.Sprintf("%s=%s", key, value))
}
cmd.Env = envVars

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

// Run the command
err = cmd.Run()

// Validate exit code
exitCode := 0
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
}
}
if exitCode != tc.Expect.ExitCode {
t.Errorf("Description: %s", tc.Description)
t.Errorf("Reason: Expected exit code %d, got %d", tc.Expect.ExitCode, exitCode)
}

// Validate stdout
if !verifyOutput(t, "stdout", stdout.String(), tc.Expect.Stdout) {
t.Errorf("Description: %s", tc.Description)
}

// Validate stderr
if !verifyOutput(t, "stderr", stderr.String(), tc.Expect.Stderr) {
t.Errorf("Description: %s", tc.Description)
}

// Validate file existence
if !verifyFileExists(t, tc.Expect.FileExists) {
t.Errorf("Description: %s", tc.Description)
}

// Validate file contents
if !verifyFileContains(t, tc.Expect.FileContains) {
t.Errorf("Description: %s", tc.Description)
}
})
}
}

func verifyOutput(t *testing.T, outputType, output string, patterns []string) bool {
success := true
for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
t.Errorf("Invalid %s regex: %q, error: %v", outputType, pattern, err)
success = false
continue
}
if !re.MatchString(output) {
t.Errorf("Reason: %s did not match pattern %q.", outputType, pattern)
t.Errorf("Output: %q", output)
success = false
}
}
return success
}

func verifyFileExists(t *testing.T, files []string) bool {
success := true
for _, file := range files {
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
t.Errorf("Reason: Expected file does not exist: %q", file)
success = false
}
}
return success
}

func verifyFileContains(t *testing.T, filePatterns map[string][]string) bool {
success := true
for file, patterns := range filePatterns {
content, err := ioutil.ReadFile(file)
if err != nil {
t.Errorf("Reason: Failed to read file %q: %v", file, err)
success = false
continue
}
for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
t.Errorf("Invalid regex for file %q: %q, error: %v", file, pattern, err)
success = false
continue
}
if !re.Match(content) {
t.Errorf("Reason: File %q did not match pattern %q.", file, pattern)
t.Errorf("Content: %q", string(content))
success = false
}
}
}
return success
}
Loading
Loading