diff --git a/.github/scripts/README.md b/.github/scripts/README.md new file mode 100644 index 0000000..496ae82 --- /dev/null +++ b/.github/scripts/README.md @@ -0,0 +1,72 @@ +# CI Scripts + +This directory contains scripts used by GitHub Actions workflows. + +## format-coverage.sh + +Formats Go coverage output as a markdown table with color-coded indicators. + +### Usage + +```bash +./format-coverage.sh [current-coverage] [main-coverage] +``` + +**Arguments:** +- `coverage-file`: Path to the Go coverage file (typically `coverage.out`) +- `current-coverage`: (optional) Overall coverage percentage for display (e.g., "74.3%") +- `main-coverage`: (optional) Main branch coverage percentage for comparison (e.g., "74.0%") + +### Examples + +**Basic usage:** +```bash +# Generate coverage file first +mise test-coverage + +# Format the coverage report +./.github/scripts/format-coverage.sh coverage.out +``` + +**With coverage percentages:** +```bash +# Calculate coverage +COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}') + +# Format with coverage display +./.github/scripts/format-coverage.sh coverage.out "$COVERAGE" +``` + +**With comparison to main:** +```bash +./.github/scripts/format-coverage.sh coverage.out "75.2%" "74.3%" +``` + +### Output Format + +The script generates a markdown report with: +- Overall coverage statistics +- Coverage comparison (if main branch coverage provided) +- Table of coverage by package with color-coded indicators: + - ๐ŸŸข Green: โ‰ฅ90% coverage + - ๐ŸŸก Yellow: โ‰ฅ75% coverage + - ๐ŸŸ  Orange: โ‰ฅ50% coverage + - ๐Ÿ”ด Red: <50% coverage +- Collapsible detailed coverage by function + +### Local Testing + +To test the output locally: + +```bash +# Run tests with coverage +mise test-coverage + +# Format and preview the output +./.github/scripts/format-coverage.sh coverage.out "74.3%" "74.3%" | less +``` + +Or save to a file for inspection: + +```bash +./.github/scripts/format-coverage.sh coverage.out "74.3%" > coverage-report.md \ No newline at end of file diff --git a/.github/scripts/format-coverage.sh b/.github/scripts/format-coverage.sh new file mode 100755 index 0000000..2c33283 --- /dev/null +++ b/.github/scripts/format-coverage.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Script to format Go coverage output as a markdown table +# Usage: ./format-coverage.sh coverage.out [current-coverage] [main-coverage] + +COVERAGE_FILE="${1:-coverage.out}" +CURRENT_COVERAGE="${2:-}" +MAIN_COVERAGE="${3:-}" + +if [ ! -f "$COVERAGE_FILE" ]; then + echo "Error: Coverage file '$COVERAGE_FILE' not found" + exit 1 +fi + +# Start markdown output +echo "## ๐Ÿ“Š Test Coverage Report" +echo "" + +# Show current and main coverage if provided +if [ -n "$CURRENT_COVERAGE" ]; then + echo "**Current Coverage:** \`$CURRENT_COVERAGE\`" + + if [ -n "$MAIN_COVERAGE" ]; then + echo "**Main Branch Coverage:** \`$MAIN_COVERAGE\`" + echo "" + + # Calculate difference + CURRENT_NUM=$(echo "$CURRENT_COVERAGE" | sed 's/%//') + MAIN_NUM=$(echo "$MAIN_COVERAGE" | sed 's/%//') + + if [ -n "$CURRENT_NUM" ] && [ -n "$MAIN_NUM" ]; then + DIFF=$(echo "$CURRENT_NUM - $MAIN_NUM" | bc -l 2>/dev/null || echo "0") + + if [ "$(echo "$DIFF > 0" | bc -l 2>/dev/null || echo "0")" = "1" ]; then + echo "**Coverage Change:** ๐Ÿ“ˆ +${DIFF}% (improved)" + elif [ "$(echo "$DIFF < 0" | bc -l 2>/dev/null || echo "0")" = "1" ]; then + DIFF_ABS=$(echo "$DIFF * -1" | bc -l 2>/dev/null || echo "${DIFF#-}") + echo "**Coverage Change:** ๐Ÿ“‰ -${DIFF_ABS}% (decreased)" + else + echo "**Coverage Change:** โœ… No change" + fi + fi + fi +fi + +echo "" +echo "### Coverage by Package" +echo "" + +# Create table header +echo "| Package | Coverage |" +echo "|---------|----------|" + +# Parse coverage and group by package +go tool cover -func="$COVERAGE_FILE" | grep -E '\.go:[0-9]+:' | \ +awk -F: '{ + # Extract package path from filename + split($1, parts, "/"); + pkg = ""; + for(i=1; i 0) ? sum/count : 0; + + # Format package name (remove common prefix) + display_pkg = pkg; + gsub(/github\.com\/speakeasy-api\/openapi\//, "", display_pkg); + + # Add emoji indicators based on coverage level + emoji = "๐Ÿ”ด"; + if(avg >= 90) emoji = "๐ŸŸข"; + else if(avg >= 75) emoji = "๐ŸŸก"; + else if(avg >= 50) emoji = "๐ŸŸ "; + + # Output with coverage value for sorting + printf "%.1f|`%s`|%s\n", avg, display_pkg, emoji; + } +}' | sort -n | awk -F'|' '{ + # Re-format after sorting by coverage + printf "| %s | %s %.1f%% |\n", $2, $3, $1; +}' + +echo "" +echo "
" +echo "๐Ÿ“‹ Detailed Coverage by Function (click to expand)" +echo "" +echo "\`\`\`" +go tool cover -func="$COVERAGE_FILE" +echo "\`\`\`" +echo "
" +echo "" +echo "- ๐Ÿงช All tests passed" +echo "- ๐Ÿ“ˆ Full coverage report available in workflow artifacts" +echo "" +echo "_Generated by GitHub Actions_" \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 14ff321..fafdc4f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -190,47 +190,11 @@ jobs: - name: Generate coverage summary id: coverage-summary run: | - echo "## ๐Ÿ“Š Test Coverage Report" > coverage-summary.md - echo "" >> coverage-summary.md - - # Current coverage + # Use the formatting script CURRENT_COV="${{ steps.coverage.outputs.coverage }}" - echo "**Current Coverage:** \`$CURRENT_COV\`" >> coverage-summary.md - - # Compare with main if this is a PR - if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ steps.main-coverage.outputs.main-coverage }}" != "" ]; then - MAIN_COV="${{ steps.main-coverage.outputs.main-coverage }}" - echo "**Main Branch Coverage:** \`$MAIN_COV\`" >> coverage-summary.md - echo "" >> coverage-summary.md - - # Calculate difference - CURRENT_NUM=$(echo $CURRENT_COV | sed 's/%//') - MAIN_NUM=$(echo $MAIN_COV | sed 's/%//') - - if [ "$CURRENT_NUM" != "" ] && [ "$MAIN_NUM" != "" ]; then - DIFF=$(echo "$CURRENT_NUM - $MAIN_NUM" | bc -l 2>/dev/null || echo "0") - - if [ "$(echo "$DIFF > 0" | bc -l 2>/dev/null)" = "1" ]; then - echo "**Coverage Change:** ๐Ÿ“ˆ +${DIFF}% (improved)" >> coverage-summary.md - elif [ "$(echo "$DIFF < 0" | bc -l 2>/dev/null)" = "1" ]; then - DIFF_ABS=$(echo "$DIFF * -1" | bc -l 2>/dev/null || echo "${DIFF#-}") - echo "**Coverage Change:** ๐Ÿ“‰ -${DIFF_ABS}% (decreased)" >> coverage-summary.md - else - echo "**Coverage Change:** โœ… No change" >> coverage-summary.md - fi - fi - fi + MAIN_COV="${{ steps.main-coverage.outputs.main-coverage }}" - echo "" >> coverage-summary.md - echo "### Coverage by Package" >> coverage-summary.md - echo "\`\`\`" >> coverage-summary.md - go tool cover -func=coverage.out >> coverage-summary.md - echo "\`\`\`" >> coverage-summary.md - echo "" >> coverage-summary.md - echo "- ๐Ÿงช All tests passed" >> coverage-summary.md - echo "- ๐Ÿ“ˆ Full coverage report available in workflow artifacts" >> coverage-summary.md - echo "" >> coverage-summary.md - echo "_Generated by GitHub Actions_" >> coverage-summary.md + ./.github/scripts/format-coverage.sh coverage.out "$CURRENT_COV" "$MAIN_COV" > coverage-summary.md - name: Upload coverage artifact uses: actions/upload-artifact@v4 diff --git a/AGENTS.md b/AGENTS.md index bcd3825..60dd922 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -218,6 +218,7 @@ import ( - Use `assert.Equal()` for value comparisons with descriptive messages - Use `assert.Nil()` and `assert.NotNil()` for pointer checks - Use `require.*` when the test should stop on failure (e.g., setup operations) +- **Use `require.Error()` for error assertions** - The linter enforces this via testifylint - **Always include descriptive error messages** ```go diff --git a/internal/interfaces/interfaces_test.go b/internal/interfaces/interfaces_test.go new file mode 100644 index 0000000..0dc1a45 --- /dev/null +++ b/internal/interfaces/interfaces_test.go @@ -0,0 +1,231 @@ +package interfaces + +import ( + "context" + "iter" + "reflect" + "testing" + + "github.com/speakeasy-api/openapi/validation" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +// Test implementations for Model interface +type testModelCore struct{} + +type testModelImpl struct { + core *testModelCore +} + +func (t *testModelImpl) Validate(ctx context.Context, opts ...validation.Option) []error { + return nil +} + +func (t *testModelImpl) GetCore() *testModelCore { + return t.core +} + +// Test implementations for CoreModel interface +type testCoreModelImpl struct{} + +func (t *testCoreModelImpl) Unmarshal(ctx context.Context, parentName string, node *yaml.Node) ([]error, error) { + return nil, nil +} + +// Test implementations for SequencedMapInterface +type testSequencedMapImpl struct { + initialized bool +} + +func (t *testSequencedMapImpl) Init() { + t.initialized = true +} + +func (t *testSequencedMapImpl) IsInitialized() bool { + return t.initialized +} + +func (t *testSequencedMapImpl) SetUntyped(key, value any) error { + return nil +} + +func (t *testSequencedMapImpl) AllUntyped() iter.Seq2[any, any] { + return func(yield func(any, any) bool) {} +} + +func (t *testSequencedMapImpl) GetKeyType() reflect.Type { + return reflect.TypeOf("") +} + +func (t *testSequencedMapImpl) GetValueType() reflect.Type { + return reflect.TypeOf("") +} + +func (t *testSequencedMapImpl) Len() int { + return 0 +} + +func (t *testSequencedMapImpl) GetAny(key any) (any, bool) { + return nil, false +} + +func (t *testSequencedMapImpl) SetAny(key, value any) {} + +func (t *testSequencedMapImpl) DeleteAny(key any) {} + +func (t *testSequencedMapImpl) KeysAny() iter.Seq[any] { + return func(yield func(any) bool) {} +} + +// Test types that do NOT implement interfaces +type testNonModel struct{} + +type testNonCoreModel struct{} + +type testNonSequencedMap struct{} + +func TestImplementsInterface_Model_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + typeToCheck reflect.Type + shouldImplement bool + }{ + { + name: "pointer to struct implements Model", + typeToCheck: reflect.TypeOf(&testModelImpl{}), + shouldImplement: true, + }, + { + name: "struct does not implement Model (needs pointer receiver)", + typeToCheck: reflect.TypeOf(testModelImpl{}), + shouldImplement: false, + }, + { + name: "non-model type does not implement Model", + typeToCheck: reflect.TypeOf(&testNonModel{}), + shouldImplement: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := ImplementsInterface[Model[testModelCore]](tt.typeToCheck) + assert.Equal(t, tt.shouldImplement, result, "should correctly identify Model implementation") + }) + } +} + +func TestImplementsInterface_CoreModel_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + typeToCheck reflect.Type + shouldImplement bool + }{ + { + name: "pointer to struct implements CoreModel", + typeToCheck: reflect.TypeOf(&testCoreModelImpl{}), + shouldImplement: true, + }, + { + name: "struct does not implement CoreModel (needs pointer receiver)", + typeToCheck: reflect.TypeOf(testCoreModelImpl{}), + shouldImplement: false, + }, + { + name: "non-core-model type does not implement CoreModel", + typeToCheck: reflect.TypeOf(&testNonCoreModel{}), + shouldImplement: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := ImplementsInterface[CoreModel](tt.typeToCheck) + assert.Equal(t, tt.shouldImplement, result, "should correctly identify CoreModel implementation") + }) + } +} + +func TestImplementsInterface_SequencedMapInterface_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + typeToCheck reflect.Type + shouldImplement bool + }{ + { + name: "pointer to struct implements SequencedMapInterface", + typeToCheck: reflect.TypeOf(&testSequencedMapImpl{}), + shouldImplement: true, + }, + { + name: "struct does not implement SequencedMapInterface (needs pointer receiver)", + typeToCheck: reflect.TypeOf(testSequencedMapImpl{}), + shouldImplement: false, + }, + { + name: "non-sequenced-map type does not implement SequencedMapInterface", + typeToCheck: reflect.TypeOf(&testNonSequencedMap{}), + shouldImplement: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := ImplementsInterface[SequencedMapInterface](tt.typeToCheck) + assert.Equal(t, tt.shouldImplement, result, "should correctly identify SequencedMapInterface implementation") + }) + } +} + +func TestImplementsInterface_BuiltInTypes_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + typeToCheck reflect.Type + shouldImplement bool + }{ + { + name: "string does not implement CoreModel", + typeToCheck: reflect.TypeOf(""), + shouldImplement: false, + }, + { + name: "int does not implement CoreModel", + typeToCheck: reflect.TypeOf(0), + shouldImplement: false, + }, + { + name: "map does not implement SequencedMapInterface", + typeToCheck: reflect.TypeOf(map[string]string{}), + shouldImplement: false, + }, + { + name: "slice does not implement CoreModel", + typeToCheck: reflect.TypeOf([]string{}), + shouldImplement: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := ImplementsInterface[CoreModel](tt.typeToCheck) + assert.Equal(t, tt.shouldImplement, result, "should correctly identify that built-in types do not implement interfaces") + }) + } +} diff --git a/internal/testutils/utils_test.go b/internal/testutils/utils_test.go new file mode 100644 index 0000000..e4f7ba0 --- /dev/null +++ b/internal/testutils/utils_test.go @@ -0,0 +1,510 @@ +package testutils + +import ( + "iter" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestCreateStringYamlNode_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value string + line int + column int + wantValue *yaml.Node + }{ + { + name: "creates string node", + value: "test", + line: 1, + column: 2, + wantValue: &yaml.Node{ + Value: "test", + Kind: yaml.ScalarNode, + Tag: "!!str", + Line: 1, + Column: 2, + }, + }, + { + name: "creates empty string node", + value: "", + line: 3, + column: 4, + wantValue: &yaml.Node{ + Value: "", + Kind: yaml.ScalarNode, + Tag: "!!str", + Line: 3, + Column: 4, + }, + }, + { + name: "creates string with special characters", + value: "hello\nworld", + line: 5, + column: 6, + wantValue: &yaml.Node{ + Value: "hello\nworld", + Kind: yaml.ScalarNode, + Tag: "!!str", + Line: 5, + Column: 6, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := CreateStringYamlNode(tt.value, tt.line, tt.column) + + assert.Equal(t, tt.wantValue, result, "should create correct string YAML node") + }) + } +} + +func TestCreateIntYamlNode_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value int + line int + column int + wantValue *yaml.Node + }{ + { + name: "creates positive int node", + value: 42, + line: 1, + column: 2, + wantValue: &yaml.Node{ + Value: "42", + Kind: yaml.ScalarNode, + Tag: "!!int", + Line: 1, + Column: 2, + }, + }, + { + name: "creates zero int node", + value: 0, + line: 3, + column: 4, + wantValue: &yaml.Node{ + Value: "0", + Kind: yaml.ScalarNode, + Tag: "!!int", + Line: 3, + Column: 4, + }, + }, + { + name: "creates negative int node", + value: -100, + line: 5, + column: 6, + wantValue: &yaml.Node{ + Value: "-100", + Kind: yaml.ScalarNode, + Tag: "!!int", + Line: 5, + Column: 6, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := CreateIntYamlNode(tt.value, tt.line, tt.column) + + assert.Equal(t, tt.wantValue, result, "should create correct int YAML node") + }) + } +} + +func TestCreateBoolYamlNode_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value bool + line int + column int + wantValue *yaml.Node + }{ + { + name: "creates true bool node", + value: true, + line: 1, + column: 2, + wantValue: &yaml.Node{ + Value: "true", + Kind: yaml.ScalarNode, + Tag: "!!bool", + Line: 1, + Column: 2, + }, + }, + { + name: "creates false bool node", + value: false, + line: 3, + column: 4, + wantValue: &yaml.Node{ + Value: "false", + Kind: yaml.ScalarNode, + Tag: "!!bool", + Line: 3, + Column: 4, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := CreateBoolYamlNode(tt.value, tt.line, tt.column) + + assert.Equal(t, tt.wantValue, result, "should create correct bool YAML node") + }) + } +} + +func TestCreateMapYamlNode_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + contents []*yaml.Node + line int + column int + wantValue *yaml.Node + }{ + { + name: "creates map node with contents", + contents: []*yaml.Node{ + CreateStringYamlNode("key1", 1, 1), + CreateStringYamlNode("value1", 1, 6), + CreateStringYamlNode("key2", 2, 1), + CreateIntYamlNode(42, 2, 6), + }, + line: 1, + column: 1, + wantValue: &yaml.Node{ + Content: []*yaml.Node{ + CreateStringYamlNode("key1", 1, 1), + CreateStringYamlNode("value1", 1, 6), + CreateStringYamlNode("key2", 2, 1), + CreateIntYamlNode(42, 2, 6), + }, + Kind: yaml.MappingNode, + Tag: "!!map", + Line: 1, + Column: 1, + }, + }, + { + name: "creates empty map node", + contents: []*yaml.Node{}, + line: 5, + column: 10, + wantValue: &yaml.Node{ + Content: []*yaml.Node{}, + Kind: yaml.MappingNode, + Tag: "!!map", + Line: 5, + Column: 10, + }, + }, + { + name: "creates nil map node", + contents: nil, + line: 3, + column: 4, + wantValue: &yaml.Node{ + Content: nil, + Kind: yaml.MappingNode, + Tag: "!!map", + Line: 3, + Column: 4, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := CreateMapYamlNode(tt.contents, tt.line, tt.column) + + assert.Equal(t, tt.wantValue, result, "should create correct map YAML node") + }) + } +} + +func TestIsInterfaceNil(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input any + wantValue bool + }{ + { + name: "returns true for nil interface", + input: nil, + wantValue: true, + }, + { + name: "returns true for nil pointer", + input: (*string)(nil), + wantValue: true, + }, + { + name: "returns true for nil slice", + input: ([]string)(nil), + wantValue: true, + }, + { + name: "returns true for nil map", + input: (map[string]string)(nil), + wantValue: true, + }, + { + name: "returns true for nil channel", + input: (chan int)(nil), + wantValue: true, + }, + { + name: "returns true for nil function", + input: (func())(nil), + wantValue: true, + }, + { + name: "returns false for non-nil string", + input: "test", + wantValue: false, + }, + { + name: "returns false for non-nil pointer", + input: new(string), + wantValue: false, + }, + { + name: "returns false for non-nil slice", + input: []string{"test"}, + wantValue: false, + }, + { + name: "returns false for non-nil map", + input: map[string]string{"key": "value"}, + wantValue: false, + }, + { + name: "returns false for zero int", + input: 0, + wantValue: false, + }, + { + name: "returns false for empty string", + input: "", + wantValue: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := isInterfaceNil(tt.input) + + assert.Equal(t, tt.wantValue, result, "should return correct nil check result") + }) + } +} + +// mockSequencedMap is a simple implementation for testing +type mockSequencedMap struct { + data map[any]any + keys []any +} + +func newMockSequencedMap(pairs ...any) *mockSequencedMap { + if len(pairs)%2 != 0 { + panic("pairs must have even length") + } + m := &mockSequencedMap{ + data: make(map[any]any), + keys: make([]any, 0, len(pairs)/2), + } + for i := 0; i < len(pairs); i += 2 { + key := pairs[i] + value := pairs[i+1] + m.data[key] = value + m.keys = append(m.keys, key) + } + return m +} + +func (m *mockSequencedMap) Len() int { + if m == nil { + return 0 + } + return len(m.keys) +} + +func (m *mockSequencedMap) AllUntyped() iter.Seq2[any, any] { + return func(yield func(any, any) bool) { + if m == nil { + return + } + for _, k := range m.keys { + if !yield(k, m.data[k]) { + return + } + } + } +} + +func (m *mockSequencedMap) GetUntyped(key any) (any, bool) { + if m == nil { + return nil, false + } + v, ok := m.data[key] + return v, ok +} + +func TestAssertEqualSequencedMap_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expected SequencedMap + actual SequencedMap + }{ + { + name: "both nil interfaces", + expected: nil, + actual: nil, + }, + { + name: "both nil underlying values", + expected: (*mockSequencedMap)(nil), + actual: (*mockSequencedMap)(nil), + }, + { + name: "equal simple maps", + expected: newMockSequencedMap("key1", "value1", "key2", "value2"), + actual: newMockSequencedMap("key1", "value1", "key2", "value2"), + }, + { + name: "equal empty maps", + expected: newMockSequencedMap(), + actual: newMockSequencedMap(), + }, + { + name: "equal maps with different types", + expected: newMockSequencedMap("string", "value", 42, 100, true, false), + actual: newMockSequencedMap("string", "value", 42, 100, true, false), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Should not panic or fail + AssertEqualSequencedMap(t, tt.expected, tt.actual) + }) + } +} + +func TestAssertEqualSequencedMap_Failure(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expected SequencedMap + actual SequencedMap + }{ + { + name: "expected nil, actual not nil", + expected: nil, + actual: newMockSequencedMap("key", "value"), + }, + { + name: "expected not nil, actual nil", + expected: newMockSequencedMap("key", "value"), + actual: nil, + }, + { + name: "different lengths", + expected: newMockSequencedMap("key1", "value1"), + actual: newMockSequencedMap("key1", "value1", "key2", "value2"), + }, + { + name: "different values", + expected: newMockSequencedMap("key", "value1"), + actual: newMockSequencedMap("key", "value2"), + }, + { + name: "missing key in actual", + expected: newMockSequencedMap("key1", "value1", "key2", "value2"), + actual: newMockSequencedMap("key1", "value1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create a mock testing.T to capture failures + mockT := &testing.T{} + + // This should cause an assertion failure but not panic + // We use require to ensure failures are detected + defer func() { + if r := recover(); r != nil { + t.Errorf("AssertEqualSequencedMap should not panic: %v", r) + } + }() + + AssertEqualSequencedMap(mockT, tt.expected, tt.actual) + }) + } +} + +func TestAssertEqualSequencedMap_NilChecks(t *testing.T) { + t.Parallel() + + t.Run("handles nil expected with nil underlying", func(t *testing.T) { + t.Parallel() + + var expected *mockSequencedMap + var actual *mockSequencedMap + + // Should not panic + AssertEqualSequencedMap(t, expected, actual) + }) + + t.Run("handles mixed nil types", func(t *testing.T) { + t.Parallel() + + mockT := &testing.T{} + var nilPtr *mockSequencedMap + + // Should detect difference between true nil and nil pointer + AssertEqualSequencedMap(mockT, nil, nilPtr) + }) +} diff --git a/mise-tasks/test-coverage b/mise-tasks/test-coverage index f2a72f5..064c44a 100755 --- a/mise-tasks/test-coverage +++ b/mise-tasks/test-coverage @@ -27,8 +27,8 @@ echo "" echo "### ๐ŸŽฏ Functions with 0% Coverage" echo "" -echo "| File:Function | Coverage |" -echo "|---------------|----------|" +echo "| File:Function | Coverage |" +echo "|------------------------------------------------------------------------|----------|" # Show functions with 0% coverage go tool cover -func=coverage.out | awk ' @@ -43,15 +43,15 @@ go tool cover -func=coverage.out | awk ' # Show functions with 0% coverage if (coverage == "0.0") { - printf "| %s | %.1f%% |\n", file_func, coverage + printf "| %-70s | %7s%% |\n", file_func, coverage } }' | head -30 echo "" echo "### ๐Ÿ” Functions with Low Coverage (< 50%)" echo "" -echo "| File:Function | Coverage |" -echo "|---------------|----------|" +echo "| File:Function | Coverage |" +echo "|------------------------------------------------------------------------|----------|" # Show functions with low coverage go tool cover -func=coverage.out | awk ' @@ -66,15 +66,15 @@ go tool cover -func=coverage.out | awk ' # Show functions with less than 50% coverage (but not 0%) if (coverage != "" && coverage > 0 && coverage < 50) { - printf "| %s | %.1f%% |\n", file_func, coverage + printf "| %-70s | %7s%% |\n", file_func, coverage } }' | head -30 echo "" echo "### ๐Ÿ“‹ Packages Needing Improvement (< 80%)" echo "" -echo "| Package | Coverage | Priority |" -echo "|---------|----------|----------|" +echo "| Package | Coverage | Priority |" +echo "|---------------------------------------------------------------|----------|-----------------|" # Show packages that need improvement go tool cover -func=coverage.out | awk ' @@ -112,10 +112,10 @@ END { if (avg_coverage > 60) priority = "๐ŸŸก Medium" if (avg_coverage > 40) priority = "๐ŸŸ  Medium-High" if (avg_coverage == 0) priority = "๐Ÿšจ Critical" - printf "| %s | %.1f%% | %s |\n", pkg, avg_coverage, priority + printf "| %-61s | %7s%% | %-14s |\n", pkg, sprintf("%.1f", avg_coverage), priority } } -}' | sort -k2 -n | head -15 +}' | sort -k4,4n | head -15 echo "" echo "---" diff --git a/overlay/loader/overlay_test.go b/overlay/loader/overlay_test.go new file mode 100644 index 0000000..1807a17 --- /dev/null +++ b/overlay/loader/overlay_test.go @@ -0,0 +1,62 @@ +package loader + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadOverlay_Success(t *testing.T) { + t.Parallel() + + // Use existing test data from overlay/testdata + testDataPath := filepath.Join("..", "testdata", "overlay-generated.yaml") + + result, err := LoadOverlay(testDataPath) + + require.NoError(t, err, "should load overlay successfully") + require.NotNil(t, result, "should return non-nil overlay") + assert.NotEmpty(t, result.Version, "overlay should have version") +} + +func TestLoadOverlay_Error_InvalidPath(t *testing.T) { + t.Parallel() + + result, err := LoadOverlay("nonexistent-file.yaml") + + require.Error(t, err, "should return error for nonexistent file") + assert.Nil(t, result, "should return nil overlay on error") + assert.Contains(t, err.Error(), "failed to parse overlay", "error should mention parsing failure") +} + +func TestLoadOverlay_Error_InvalidYAML(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + invalidFile := filepath.Join(tmpDir, "invalid.yaml") + err := os.WriteFile(invalidFile, []byte("invalid: yaml: content: ["), 0o644) + require.NoError(t, err, "should create test file") + + result, err := LoadOverlay(invalidFile) + + require.Error(t, err, "should return error for invalid YAML") + assert.Nil(t, result, "should return nil overlay on error") +} + +func TestLoadOverlay_EmptyFile(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + emptyFile := filepath.Join(tmpDir, "empty.yaml") + err := os.WriteFile(emptyFile, []byte(""), 0o644) + require.NoError(t, err, "should create test file") + + result, err := LoadOverlay(emptyFile) + + require.NoError(t, err, "should handle empty file") + require.NotNil(t, result, "should return non-nil overlay for empty file") + assert.Empty(t, result.Version, "empty file should have empty version") +} diff --git a/overlay/loader/spec.go b/overlay/loader/spec.go index 1f19463..ded022f 100644 --- a/overlay/loader/spec.go +++ b/overlay/loader/spec.go @@ -5,6 +5,9 @@ import ( "fmt" "net/url" "os" + "path/filepath" + "runtime" + "strings" "github.com/speakeasy-api/openapi/overlay" "gopkg.in/yaml.v3" @@ -19,6 +22,23 @@ func GetOverlayExtendsPath(o *overlay.Overlay) (string, error) { return "", errors.New("overlay does not specify an extends URL") } + // Handle Windows file paths that might be formatted as file URLs + // file:///C:/path or file://C:/path on Windows need special handling + if runtime.GOOS == "windows" && strings.HasPrefix(o.Extends, "file://") { + // Remove the file:// or file:/// prefix + path := strings.TrimPrefix(o.Extends, "file:///") + if path == o.Extends { + path = strings.TrimPrefix(o.Extends, "file://") + } + + // If it looks like a Windows path (e.g., C:/... or C:\...), use it directly + if len(path) >= 2 && path[1] == ':' { + // Convert forward slashes to backslashes for Windows + path = filepath.FromSlash(path) + return path, nil + } + } + specUrl, err := url.Parse(o.Extends) if err != nil { return "", fmt.Errorf("failed to parse URL %q: %w", o.Extends, err) @@ -28,7 +48,14 @@ func GetOverlayExtendsPath(o *overlay.Overlay) (string, error) { return "", fmt.Errorf("only file:// extends URLs are supported, not %q", o.Extends) } - return specUrl.Path, nil + // On Windows, url.Parse().Path for file:///C:/path returns /C:/path + // We need to strip the leading slash for Windows absolute paths + path := specUrl.Path + if runtime.GOOS == "windows" && len(path) >= 3 && path[0] == '/' && path[2] == ':' { + path = path[1:] // Remove leading slash + } + + return path, nil } // LoadExtendsSpecification will load and parse a YAML or JSON file as specified @@ -49,6 +76,7 @@ func LoadSpecification(path string) (*yaml.Node, error) { if err != nil { return nil, fmt.Errorf("failed to open schema from path %q: %w", path, err) } + defer rs.Close() var ys yaml.Node err = yaml.NewDecoder(rs).Decode(&ys) diff --git a/overlay/loader/spec_test.go b/overlay/loader/spec_test.go new file mode 100644 index 0000000..1a6b70b --- /dev/null +++ b/overlay/loader/spec_test.go @@ -0,0 +1,348 @@ +package loader + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/speakeasy-api/openapi/overlay" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// pathToFileURL converts a file path to a proper file:// URL +// that works cross-platform (Windows and Unix) +func pathToFileURL(path string) string { + // Convert backslashes to forward slashes for Windows + path = filepath.ToSlash(path) + + // On Windows, absolute paths start with a drive letter (e.g., C:/) + // For file URLs, we need file:///C:/path format (three slashes) + if runtime.GOOS == "windows" && len(path) >= 2 && path[1] == ':' { + return "file:///" + path + } + + // On Unix, absolute paths start with / + // For file URLs, we need file:///path format (three slashes) + if strings.HasPrefix(path, "/") { + return "file://" + path + } + + // Relative paths + return "file:///" + path +} + +func TestGetOverlayExtendsPath_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + overlay *overlay.Overlay + wantValue string + }{ + { + name: "returns path from file URL", + overlay: &overlay.Overlay{ + Extends: "file:///path/to/spec.yaml", + }, + wantValue: "/path/to/spec.yaml", + }, + { + name: "returns path from file URL with host", + overlay: &overlay.Overlay{ + Extends: "file://localhost/path/to/spec.yaml", + }, + wantValue: "/path/to/spec.yaml", + }, + { + name: "returns decoded path from file URL", + overlay: &overlay.Overlay{ + Extends: "file:///path/to/my%20spec.yaml", + }, + wantValue: "/path/to/my spec.yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result, err := GetOverlayExtendsPath(tt.overlay) + + require.NoError(t, err, "should get path successfully") + assert.Equal(t, tt.wantValue, result, "should return correct path") + }) + } +} + +func TestGetOverlayExtendsPath_Error(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + overlay *overlay.Overlay + wantErrorMsg string + }{ + { + name: "returns error when extends is empty", + overlay: &overlay.Overlay{ + Extends: "", + }, + wantErrorMsg: "overlay does not specify an extends URL", + }, + { + name: "returns error for http URL", + overlay: &overlay.Overlay{ + Extends: "http://example.com/spec.yaml", + }, + wantErrorMsg: "only file:// extends URLs are supported", + }, + { + name: "returns error for https URL", + overlay: &overlay.Overlay{ + Extends: "https://example.com/spec.yaml", + }, + wantErrorMsg: "only file:// extends URLs are supported", + }, + { + name: "returns error for invalid URL", + overlay: &overlay.Overlay{ + Extends: "://invalid-url", + }, + wantErrorMsg: "failed to parse URL", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result, err := GetOverlayExtendsPath(tt.overlay) + + require.Error(t, err, "should return error") + assert.Empty(t, result, "should return empty path on error") + assert.Contains(t, err.Error(), tt.wantErrorMsg, "error should contain expected message") + }) + } +} + +func TestLoadSpecification_Success(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlContent := ` +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +` + testFile := filepath.Join(tmpDir, "spec.yaml") + err := os.WriteFile(testFile, []byte(yamlContent), 0o644) + require.NoError(t, err, "should create test file") + + result, err := LoadSpecification(testFile) + + require.NoError(t, err, "should load specification successfully") + require.NotNil(t, result, "should return non-nil node") + assert.Equal(t, yaml.DocumentNode, result.Kind, "should be a document node") +} + +func TestLoadSpecification_JSONFile(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + jsonContent := `{ + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": {} +}` + testFile := filepath.Join(tmpDir, "spec.json") + err := os.WriteFile(testFile, []byte(jsonContent), 0o644) + require.NoError(t, err, "should create test file") + + result, err := LoadSpecification(testFile) + + require.NoError(t, err, "should load JSON specification successfully") + require.NotNil(t, result, "should return non-nil node") + assert.Equal(t, yaml.DocumentNode, result.Kind, "should be a document node") +} + +func TestLoadSpecification_Error_FileNotFound(t *testing.T) { + t.Parallel() + + result, err := LoadSpecification("nonexistent-file.yaml") + + require.Error(t, err, "should return error for nonexistent file") + assert.Nil(t, result, "should return nil node on error") + assert.Contains(t, err.Error(), "failed to open schema", "error should mention opening failure") +} + +func TestLoadSpecification_Error_InvalidYAML(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "invalid.yaml") + err := os.WriteFile(testFile, []byte("invalid: yaml: [content"), 0o644) + require.NoError(t, err, "should create test file") + + result, err := LoadSpecification(testFile) + + require.Error(t, err, "should return error for invalid YAML") + assert.Nil(t, result, "should return nil node on error") + assert.Contains(t, err.Error(), "failed to parse schema", "error should mention parsing failure") +} + +func TestLoadExtendsSpecification_Success(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlContent := ` +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +` + testFile := filepath.Join(tmpDir, "spec.yaml") + err := os.WriteFile(testFile, []byte(yamlContent), 0o644) + require.NoError(t, err, "should create test file") + + o := &overlay.Overlay{ + Extends: pathToFileURL(testFile), + } + + result, err := LoadExtendsSpecification(o) + + require.NoError(t, err, "should load extends specification successfully") + require.NotNil(t, result, "should return non-nil node") + assert.Equal(t, yaml.DocumentNode, result.Kind, "should be a document node") +} + +func TestLoadExtendsSpecification_Error_NoExtends(t *testing.T) { + t.Parallel() + + o := &overlay.Overlay{ + Extends: "", + } + + result, err := LoadExtendsSpecification(o) + + require.Error(t, err, "should return error when extends is empty") + assert.Nil(t, result, "should return nil node on error") + assert.Contains(t, err.Error(), "overlay does not specify an extends URL", "error should mention missing extends") +} + +func TestLoadExtendsSpecification_Error_InvalidURL(t *testing.T) { + t.Parallel() + + o := &overlay.Overlay{ + Extends: "http://example.com/spec.yaml", + } + + result, err := LoadExtendsSpecification(o) + + require.Error(t, err, "should return error for non-file URL") + assert.Nil(t, result, "should return nil node on error") + assert.Contains(t, err.Error(), "only file:// extends URLs are supported", "error should mention unsupported URL scheme") +} + +func TestLoadExtendsSpecification_Error_FileNotFound(t *testing.T) { + t.Parallel() + + o := &overlay.Overlay{ + Extends: "file:///nonexistent/spec.yaml", + } + + result, err := LoadExtendsSpecification(o) + + require.Error(t, err, "should return error for nonexistent file") + assert.Nil(t, result, "should return nil node on error") + assert.Contains(t, err.Error(), "failed to open schema", "error should mention file opening failure") +} + +func TestLoadEitherSpecification_WithPath(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlContent := ` +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +` + testFile := filepath.Join(tmpDir, "spec.yaml") + err := os.WriteFile(testFile, []byte(yamlContent), 0o644) + require.NoError(t, err, "should create test file") + + o := &overlay.Overlay{ + Extends: "file:///some/other/path.yaml", + } + + result, name, err := LoadEitherSpecification(testFile, o) + + require.NoError(t, err, "should load specification from provided path") + require.NotNil(t, result, "should return non-nil node") + assert.Equal(t, testFile, name, "should return provided path as name") + assert.Equal(t, yaml.DocumentNode, result.Kind, "should be a document node") +} + +func TestLoadEitherSpecification_WithoutPath(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlContent := ` +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +` + testFile := filepath.Join(tmpDir, "spec.yaml") + err := os.WriteFile(testFile, []byte(yamlContent), 0o644) + require.NoError(t, err, "should create test file") + + o := &overlay.Overlay{ + Extends: pathToFileURL(testFile), + } + + result, name, err := LoadEitherSpecification("", o) + + require.NoError(t, err, "should load specification from extends URL") + require.NotNil(t, result, "should return non-nil node") + assert.Equal(t, testFile, name, "should return extends path as name") + assert.Equal(t, yaml.DocumentNode, result.Kind, "should be a document node") +} + +func TestLoadEitherSpecification_Error_NoPathNoExtends(t *testing.T) { + t.Parallel() + + o := &overlay.Overlay{ + Extends: "", + } + + result, name, err := LoadEitherSpecification("", o) + + require.Error(t, err, "should return error when neither path nor extends provided") + assert.Nil(t, result, "should return nil node on error") + assert.Empty(t, name, "should return empty name on error") +} + +func TestLoadEitherSpecification_Error_InvalidPath(t *testing.T) { + t.Parallel() + + o := &overlay.Overlay{} + + result, name, err := LoadEitherSpecification("nonexistent-file.yaml", o) + + require.Error(t, err, "should return error for invalid path") + assert.Nil(t, result, "should return nil node on error") + assert.Equal(t, "nonexistent-file.yaml", name, "should return attempted path as name") +} diff --git a/pointer/pointer_test.go b/pointer/pointer_test.go new file mode 100644 index 0000000..842bce3 --- /dev/null +++ b/pointer/pointer_test.go @@ -0,0 +1,284 @@ +package pointer + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFrom_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input any + wantValue any + }{ + { + name: "creates pointer from string", + input: "test", + wantValue: From("test"), + }, + { + name: "creates pointer from empty string", + input: "", + wantValue: From(""), + }, + { + name: "creates pointer from int", + input: 42, + wantValue: From(42), + }, + { + name: "creates pointer from zero int", + input: 0, + wantValue: From(0), + }, + { + name: "creates pointer from negative int", + input: -1, + wantValue: From(-1), + }, + { + name: "creates pointer from bool true", + input: true, + wantValue: From(true), + }, + { + name: "creates pointer from bool false", + input: false, + wantValue: From(false), + }, + { + name: "creates pointer from float64", + input: 3.14, + wantValue: From(3.14), + }, + { + name: "creates pointer from zero float64", + input: 0.0, + wantValue: From(0.0), + }, + { + name: "creates pointer from negative float64", + input: -2.5, + wantValue: From(-2.5), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var result any + switch v := tt.input.(type) { + case string: + result = From(v) + case int: + result = From(v) + case bool: + result = From(v) + case float64: + result = From(v) + } + + assert.Equal(t, tt.wantValue, result, "should return correct pointer value") + }) + } +} + +func TestFrom_Struct(t *testing.T) { + t.Parallel() + + type testStruct struct { + Name string + Value int + } + + input := testStruct{Name: "test", Value: 42} + wantValue := From(testStruct{Name: "test", Value: 42}) + result := From(input) + + assert.Equal(t, wantValue, result, "should return correct pointer value") +} + +func TestValue_Success(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input any + wantValue any + }{ + { + name: "returns value from string pointer", + input: From("test"), + wantValue: "test", + }, + { + name: "returns value from int pointer", + input: From(42), + wantValue: 42, + }, + { + name: "returns value from bool pointer", + input: From(true), + wantValue: true, + }, + { + name: "returns value from float64 pointer", + input: From(3.14), + wantValue: 3.14, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var result any + switch v := tt.input.(type) { + case *string: + result = Value(v) + case *int: + result = Value(v) + case *bool: + result = Value(v) + case *float64: + result = Value(v) + } + + assert.Equal(t, tt.wantValue, result, "should return correct value") + }) + } +} + +func TestValue_ReturnsZeroForNil(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input any + wantValue any + }{ + { + name: "returns empty string for nil string pointer", + input: (*string)(nil), + wantValue: "", + }, + { + name: "returns zero for nil int pointer", + input: (*int)(nil), + wantValue: 0, + }, + { + name: "returns false for nil bool pointer", + input: (*bool)(nil), + wantValue: false, + }, + { + name: "returns zero for nil float64 pointer", + input: (*float64)(nil), + wantValue: 0.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var result any + switch tt.input.(type) { + case *string: + result = Value((*string)(nil)) + case *int: + result = Value((*int)(nil)) + case *bool: + result = Value((*bool)(nil)) + case *float64: + result = Value((*float64)(nil)) + } + + assert.Equal(t, tt.wantValue, result, "should return zero value") + }) + } +} + +func TestValue_Struct(t *testing.T) { + t.Parallel() + + type testStruct struct { + Name string + Value int + } + + t.Run("returns value from struct pointer", func(t *testing.T) { + t.Parallel() + + input := From(testStruct{Name: "test", Value: 42}) + wantValue := testStruct{Name: "test", Value: 42} + result := Value(input) + + assert.Equal(t, wantValue, result, "should return correct struct value") + }) + + t.Run("returns zero struct for nil pointer", func(t *testing.T) { + t.Parallel() + + wantValue := testStruct{} + result := Value((*testStruct)(nil)) + + assert.Equal(t, wantValue, result, "should return zero struct value") + }) +} + +func TestPointer_RoundTrip(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value any + wantValue any + }{ + { + name: "string round trip", + value: "test", + wantValue: "test", + }, + { + name: "int round trip", + value: 42, + wantValue: 42, + }, + { + name: "bool round trip", + value: true, + wantValue: true, + }, + { + name: "float64 round trip", + value: 3.14, + wantValue: 3.14, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var result any + switch v := tt.value.(type) { + case string: + result = Value(From(v)) + case int: + result = Value(From(v)) + case bool: + result = Value(From(v)) + case float64: + result = Value(From(v)) + } + + assert.Equal(t, tt.wantValue, result, "should match original value after round trip") + }) + } +} diff --git a/system/filesystem_test.go b/system/filesystem_test.go new file mode 100644 index 0000000..a48f913 --- /dev/null +++ b/system/filesystem_test.go @@ -0,0 +1,176 @@ +package system + +import ( + "io/fs" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFileSystem_Open_Success(t *testing.T) { + t.Parallel() + + // Create a temporary file for testing + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.txt") + testContent := []byte("test content") + err := os.WriteFile(testFile, testContent, 0o644) + require.NoError(t, err, "should create test file") + + fsys := &FileSystem{} + file, err := fsys.Open(testFile) + + require.NoError(t, err, "should open file successfully") + require.NotNil(t, file, "should return non-nil file") + defer file.Close() + + // Verify file can be read + content := make([]byte, len(testContent)) + n, err := file.Read(content) + require.NoError(t, err, "should read file content") + assert.Equal(t, len(testContent), n, "should read correct number of bytes") + assert.Equal(t, testContent, content, "should read correct content") +} + +func TestFileSystem_Open_Error(t *testing.T) { + t.Parallel() + + fsys := &FileSystem{} + file, err := fsys.Open("nonexistent-file.txt") + + require.Error(t, err, "should return error for nonexistent file") + assert.Nil(t, file, "should return nil file on error") +} + +func TestFileSystem_WriteFile_Success(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "subdir", "test.txt") + testContent := []byte("test content") + + fsys := &FileSystem{} + err := fsys.WriteFile(testFile, testContent, 0o644) + + require.NoError(t, err, "should write file successfully") + + // Verify file was written + content, err := os.ReadFile(testFile) + require.NoError(t, err, "should read written file") + assert.Equal(t, testContent, content, "should have correct content") + + // Verify file permissions (Unix only - Windows doesn't support Unix-style permissions) + if runtime.GOOS != "windows" { + info, err := os.Stat(testFile) + require.NoError(t, err, "should stat file") + assert.Equal(t, fs.FileMode(0o644), info.Mode().Perm(), "should have correct permissions") + } +} + +func TestFileSystem_WriteFile_CreatesDirectories(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "level1", "level2", "level3", "test.txt") + testContent := []byte("test content") + + fsys := &FileSystem{} + err := fsys.WriteFile(testFile, testContent, 0o644) + + require.NoError(t, err, "should write file and create directories") + + // Verify directories were created + dirInfo, err := os.Stat(filepath.Dir(testFile)) + require.NoError(t, err, "should stat created directory") + assert.True(t, dirInfo.IsDir(), "should be a directory") + + // Verify file was written + content, err := os.ReadFile(testFile) + require.NoError(t, err, "should read written file") + assert.Equal(t, testContent, content, "should have correct content") +} + +func TestFileSystem_WriteFile_OverwritesExisting(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.txt") + + // Write initial content + initialContent := []byte("initial content") + err := os.WriteFile(testFile, initialContent, 0o644) + require.NoError(t, err, "should create initial file") + + // Overwrite with new content + newContent := []byte("new content") + fsys := &FileSystem{} + err = fsys.WriteFile(testFile, newContent, 0o644) + + require.NoError(t, err, "should overwrite file successfully") + + // Verify new content + content, err := os.ReadFile(testFile) + require.NoError(t, err, "should read file") + assert.Equal(t, newContent, content, "should have new content") +} + +func TestFileSystem_MkdirAll_Success(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + testPath := filepath.Join(tmpDir, "level1", "level2", "level3") + + fsys := &FileSystem{} + err := fsys.MkdirAll(testPath, 0o755) + + require.NoError(t, err, "should create directories successfully") + + // Verify directories were created + info, err := os.Stat(testPath) + require.NoError(t, err, "should stat created directory") + assert.True(t, info.IsDir(), "should be a directory") + + // Verify directory permissions (Unix only - Windows doesn't support Unix-style permissions) + if runtime.GOOS != "windows" { + assert.Equal(t, fs.FileMode(0o755), info.Mode().Perm(), "should have correct permissions") + } +} + +func TestFileSystem_MkdirAll_ExistingDirectory(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + testPath := filepath.Join(tmpDir, "existing") + + // Create directory first + err := os.MkdirAll(testPath, 0o755) + require.NoError(t, err, "should create initial directory") + + // Call MkdirAll on existing directory + fsys := &FileSystem{} + err = fsys.MkdirAll(testPath, 0o755) + + require.NoError(t, err, "should succeed for existing directory") + + // Verify directory still exists + info, err := os.Stat(testPath) + require.NoError(t, err, "should stat directory") + assert.True(t, info.IsDir(), "should be a directory") +} + +func TestFileSystem_ImplementsInterfaces(t *testing.T) { + t.Parallel() + + fsys := &FileSystem{} + + // Test VirtualFS interface + var _ VirtualFS = fsys + var _ fs.FS = fsys + + // Test WritableVirtualFS interface + var _ WritableVirtualFS = fsys +}