Skip to content

Conversation

@aaguiarz
Copy link
Member

@aaguiarz aaguiarz commented Oct 4, 2025

Description

What problem is being solved?

Fixes #582

How is it being solved?

Accepting multiple --file parameters so it properly handles wildcards expanded by the shell.

What changes are made to solve it?

References

Review Checklist

  • I have clicked on "allow edits by maintainers".
  • I have added documentation for new/changed functionality in this PR or in a PR to openfga.dev [Provide a link to any relevant PRs in the references section above]
  • The correct base branch is being used, if not main
  • I have added tests to validate that the change in functionality is working as expected

Summary by CodeRabbit

  • New Features
    • Test-file selection now accepts multiple entries, glob patterns, literal paths, and shell-expanded inputs with clearer errors when no matches are found.
  • Documentation
    • Added a nested example YAML demonstrating a minimal model, tuple, and test assertion.
  • Tests
    • Added comprehensive unit tests for pattern expansion, matching behavior, and error cases.
  • Chores
    • Updated ignore rules to exclude macOS .DS_Store files in test fixtures.

@aaguiarz aaguiarz requested a review from a team as a code owner October 4, 2025 22:41
@aaguiarz aaguiarz changed the title fix: handle glob patterns when expanded by the shell too fix: handle glob patterns when expanded by the shell Oct 4, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 4, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Replaces single-string test file handling with multi-pattern expansion: adds expandTestFilePatterns, switches the --tests flag to a StringArray, merges flag patterns with positional args, validates and expands globs/literals, emits precise errors for missing/invalid/no-match cases, adds unit tests and an example YAML, and updates .gitignore to ignore .DS_Store in test fixtures.

Changes

Cohort / File(s) Summary
CLI: model test pattern expansion
cmd/model/test.go
Switches --tests from String to StringArray, wires positional args into resolution, adds package-level error variables, and implements expandTestFilePatterns(patterns []string, posArgs []string) ([]string, error) to validate, expand globs, and aggregate test file paths with explicit error returns.
Unit tests & example data
cmd/model/test_test.go, example/subdir/simple.fga.yaml
Adds extensive unit tests for expandTestFilePatterns covering literals, multiple patterns, shell-expanded positional args, quoted/unquoted globs, mixed inputs, invalid patterns, no-match and no-files errors; adds a minimal example YAML used by tests.
Repo config
.gitignore
Adds rule to ignore .DS_Store under tests/fixtures.*

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as "fga model test CLI"
  participant Expander as "expandTestFilePatterns"
  participant FS as "Filesystem"
  Note over CLI,Expander: New flow merges flags + positional args
  User->>CLI: fga model test --tests *.fga.yaml [positional...]
  CLI->>Expander: patterns (flags) + posArgs
  alt patterns empty
    Expander-->>CLI: error "no test files specified"
  else
    loop each input
      Expander->>FS: check existence or Glob(pattern)
      FS-->>Expander: matched files / not found / glob error
    end
    alt invalid glob
      Expander-->>CLI: error "invalid glob pattern"
    else no matches
      Expander-->>CLI: error "no matching test files found"
    else matches found
      Expander-->>CLI: []files
      CLI->>CLI: run tests on files
      CLI-->>User: test results
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • sergiught
  • rhamzeh

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR includes unrelated updates such as a .gitignore rule for .DS_Store and the addition of an example YAML file under example/subdir, which are not necessary for fixing shell glob expansion handling as described in issue #582. These changes should be removed or moved into a separate commit or PR so that this pull request remains focused on implementing the shell glob pattern handling fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the main change by indicating that glob patterns expanded by the shell are now handled, matching the core update in the PR without extraneous detail.
Linked Issues Check ✅ Passed The implementation introduces a StringArray flag, expandTestFilePatterns logic, and comprehensive tests to ensure that all files from shell-expanded glob patterns are processed, directly satisfying issue #582’s requirement to handle unquoted glob expansions.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
.gitignore (1)

38-38: Consider a more comprehensive ignore pattern.

The current rule .DS_Store only ignores the file at the repository root. macOS can create .DS_Store files in any directory.

Apply this diff for better coverage:

-/tests/fixtures/identifiers
-.DS_Store
+/tests/fixtures/identifiers
+**/.DS_Store
cmd/model/test_test.go (2)

156-177: Consider reusing existing utilities and simplifying helper logic.

The helper functions duplicate functionality available in internal/slices.Contains (as shown in the code snippets). Additionally, containsSubstring at line 167 has redundant logic—checking if the string ends with the substring OR contains it anywhere, when the contains call already handles both cases.

For containsPath, consider using the existing utility:

+import "github.com/openfga/cli/internal/slices"
+
-func containsPath(files []string, pathSubstring string) bool {
-	for _, file := range files {
-		if containsSubstring(file, pathSubstring) {
-			return true
-		}
-	}
-	return false
-}
-
-func containsSubstring(s, substr string) bool {
-	return len(s) >= len(substr) && (s[len(s)-len(substr):] == substr || contains(s, substr))
-}
-
-func contains(s, substr string) bool {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
-}
+func containsPath(files []string, pathSubstring string) bool {
+	for _, file := range files {
+		if strings.Contains(file, pathSubstring) {
+			return true
+		}
+	}
+
+	return false
+}

Or even simpler, use the standard library's strings.Contains directly in test assertions.


156-177: Address linter feedback.

The linter flags minor style issues: missing periods in comments and missing blank lines before returns.

Apply this diff:

-// containsPath checks if any file in the slice contains the given path substring
+// containsPath checks if any file in the slice contains the given path substring.
 func containsPath(files []string, pathSubstring string) bool {
 	for _, file := range files {
 		if containsSubstring(file, pathSubstring) {
 			return true
 		}
 	}
+
 	return false
 }
 
 func containsSubstring(s, substr string) bool {
 	return len(s) >= len(substr) && (s[len(s)-len(substr):] == substr || contains(s, substr))
 }
 
 func contains(s, substr string) bool {
 	for i := 0; i <= len(s)-len(substr); i++ {
 		if s[i:i+len(substr)] == substr {
 			return true
 		}
 	}
+
 	return false
 }
cmd/model/test.go (2)

185-185: Fix line length violation.

The flag usage description exceeds the 120-character limit (143 characters).

Apply this diff to break the line:

-	modelTestCmd.Flags().StringArray("tests", []string{}, "Path or glob of YAML test files. Can be specified multiple times or use glob patterns")
+	modelTestCmd.Flags().StringArray("tests", []string{},
+		"Path or glob of YAML test files. Can be specified multiple times or use glob patterns")

150-150: Optional: Address remaining linter feedback.

The linter flags a few minor style issues:

  • Dynamic error messages (lines 150, 171, 176): For user-facing CLI errors, the current approach is acceptable, but you could wrap static sentinel errors for consistency with project conventions if preferred
  • Missing whitespace before range loop (line 154): Add a blank line before the loop
  • Missing blank line before continue (line 158): Add a blank line before continue

These are low-priority style improvements that can be deferred if not aligned with project conventions.

Also applies to: 154-154, 158-158, 171-171, 176-176

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87ce15a and 402e3bc.

📒 Files selected for processing (4)
  • .gitignore (1 hunks)
  • cmd/model/test.go (3 hunks)
  • cmd/model/test_test.go (1 hunks)
  • example/subdir/simple.fga.yaml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
cmd/model/test_test.go (1)
internal/slices/contains.go (1)
  • Contains (22-30)
🪛 GitHub Check: Lints
cmd/model/test_test.go

[failure] 176-176:
return with no blank line before (nlreturn)


[failure] 163-163:
return with no blank line before (nlreturn)


[failure] 156-156:
Comment should end in a period (godot)

cmd/model/test.go

[failure] 154-154:
missing whitespace above this line (no shared variables above range) (wsl_v5)


[failure] 158-158:
continue with no blank line before (nlreturn)


[failure] 185-185:
The line is 143 characters long, which exceeds the maximum of 120 characters. (lll)


[failure] 147-147:
appendAssign: append result not assigned to the same slice (gocritic)


[failure] 176-176:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("no test files found")" (err113)


[failure] 171-171:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("test file %s does not exist", pattern)" (err113)


[failure] 150-150:
do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf("no test files specified")" (err113)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests
🔇 Additional comments (5)
example/subdir/simple.fga.yaml (1)

1-23: LGTM!

The test file structure is well-formed and provides a clear, minimal example for testing subdirectory glob patterns. The model, tuples, and test assertions are consistent with each other.

cmd/model/test_test.go (1)

10-154: LGTM!

The test coverage is comprehensive and validates all key scenarios for pattern expansion including single files, multiple files, glob patterns, shell-expanded inputs, error cases, and edge cases. This provides strong confidence in the new functionality.

cmd/model/test.go (3)

39-39: LGTM!

The changes correctly implement the fix for handling shell-expanded glob patterns:

  • Allowing zero positional arguments (line 39) handles the case when all files come from the flag
  • Switching to StringArray (line 42) enables multiple --tests arguments
  • Delegating to expandTestFilePatterns (line 58) centralizes the pattern expansion logic

Also applies to: 42-42, 57-61


138-180: LGTM!

The expandTestFilePatterns function correctly handles the core requirements:

  • Combines flag patterns and positional arguments to support shell expansion (lines 143-147)
  • Validates file existence before attempting glob expansion (lines 156-159)
  • Expands glob patterns when files don't exist as literals (lines 162-172)
  • Provides clear error messages for common failure modes

147-147: Linter warning is a false positive.

The appendAssign warning at line 147 can be safely ignored. The code intentionally creates a new slice allPatterns by combining patterns and posArgs, rather than mutating patterns in place. This is the correct approach.

@aaguiarz
Copy link
Member Author

aaguiarz commented Oct 4, 2025

@claude

@aaguiarz
Copy link
Member Author

aaguiarz commented Oct 4, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 4, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
cmd/model/test.go (1)

145-191: Consider deduplicating file paths to avoid processing the same file multiple times.

The function doesn't deduplicate paths, so if a file is matched by multiple patterns or appears in both flags and positional args, it will be processed multiple times. While this might be intentional in some cases, it's more likely to be unintended behavior that could confuse users.

Apply this diff to add deduplication:

 func expandTestFilePatterns(patterns []string, posArgs []string) ([]string, error) {
 	// Combine flag values and positional args
 	// This handles shell expansion: when the shell expands ./example/*.fga.yaml to
 	// ./example/file1.yaml ./example/file2.yaml, the first file goes to the --tests flag
 	// and the rest end up as positional arguments
 	allPatterns := make([]string, 0, len(patterns)+len(posArgs))
 	allPatterns = append(allPatterns, patterns...)
 	allPatterns = append(allPatterns, posArgs...)
 
 	if len(allPatterns) == 0 {
 		return nil, errNoTestFilesSpecified
 	}
 
-	fileNames := []string{}
+	// Use a map to track seen files and maintain order with a slice
+	seen := make(map[string]bool)
+	fileNames := []string{}
 
 	for _, pattern := range allPatterns {
 		// First, check if it's a literal file that exists
 		if _, err := os.Stat(pattern); err == nil {
-			fileNames = append(fileNames, pattern)
+			if !seen[pattern] {
+				seen[pattern] = true
+				fileNames = append(fileNames, pattern)
+			}
 
 			continue
 		}
 
 		// Otherwise, try to expand it as a glob pattern
 		matches, err := filepath.Glob(pattern)
 		if err != nil {
 			return nil, fmt.Errorf("invalid tests pattern %s due to %w", pattern, err)
 		}
 
 		if len(matches) > 0 {
-			fileNames = append(fileNames, matches...)
+			for _, match := range matches {
+				if !seen[match] {
+					seen[match] = true
+					fileNames = append(fileNames, match)
+				}
+			}
 		} else {
 			// If glob didn't match and file doesn't exist, report error
 			return nil, fmt.Errorf("%w: %s", errTestFileDoesNotExist, pattern)
 		}
 	}
 
 	if len(fileNames) == 0 {
 		return nil, errNoTestFilesFound
 	}
 
 	return fileNames, nil
 }
cmd/model/test_test.go (1)

157-166: Optional: Consider more precise path matching.

The anyContains helper uses substring matching, which could theoretically cause false positives if one filename is a substring of another (e.g., "file.yaml" would match "myfile.yaml"). However, given the distinctive test paths used in this test suite, this is unlikely to be an issue in practice.

If you want more precision, you could match against the basename or use exact matching:

-// anyContains checks if any string in the slice contains the given substring.
-func anyContains(slice []string, substr string) bool {
+// anyContains checks if any string in the slice has a basename that contains the given substring.
+func anyContains(slice []string, substr string) bool {
 	for _, s := range slice {
-		if strings.Contains(s, substr) {
+		if strings.Contains(filepath.Base(s), substr) {
 			return true
 		}
 	}
 
 	return false
 }

However, the current implementation is adequate for these tests.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 402e3bc and c52806d.

📒 Files selected for processing (2)
  • cmd/model/test.go (4 hunks)
  • cmd/model/test_test.go (1 hunks)
🔇 Additional comments (4)
cmd/model/test.go (3)

34-38: LGTM!

The sentinel error declarations follow Go best practices for error signaling and provide clear, specific error messages for different failure scenarios.


46-68: LGTM!

The changes correctly handle the transition to accepting multiple file patterns:

  • Allowing zero positional args makes sense since patterns can come from the --tests flag
  • Using GetStringArray aligns with the new flag type
  • Proper error handling is in place

196-197: LGTM!

The flag definition correctly uses StringArray to accept multiple patterns, and the description clearly communicates the new capability.

cmd/model/test_test.go (1)

11-155: Excellent test coverage!

The test suite comprehensively covers the key scenarios:

  • Single and multiple file handling
  • Glob pattern expansion
  • Shell expansion simulation (flags vs positional args)
  • Mixed glob and literal inputs
  • Error conditions (non-existent files, no matches, invalid globs)

The tests effectively validate the intended behavior of handling both quoted glob patterns (CLI expands) and shell-expanded arguments.

@dyeam0 dyeam0 added the bug Something isn't working label Oct 8, 2025
@rhamzeh rhamzeh removed the bug Something isn't working label Oct 8, 2025
@rhamzeh rhamzeh added this pull request to the merge queue Oct 8, 2025
Merged via the queue into main with commit c15d6d1 Oct 8, 2025
22 checks passed
@rhamzeh rhamzeh deleted the fix/glob-patterns branch October 8, 2025 23:39
@rhamzeh
Copy link
Member

rhamzeh commented Oct 8, 2025

I know I approved and merged this, but the more I think about it, the less I like it.

This currently confuses the args of the test command with the args of the tests flag:

e.g. when we say:

fga model test --tests file1.yaml file2.yaml 

This means:

  • command: fga model test
  • args: file2.yaml
  • tests args: file1.yaml

When we do:

fga model test --tests file*.yaml file2.yaml 

in a directory with 4 files: file{1,2,3,4}.yaml

This means:

  • command: fga model test
  • args: file2.yaml file3.yaml file4.yaml file2.yaml
  • tests args: file1.yaml

Given that - I would rather either:

  1. revert this PR and consider the issue a non-issue
  2. keep the tests as an array part of this PR as that's still valuable but still consider the shell expansion a non-issue
  3. deprecate tests and rely on command args as the primary interface. This would look like the current PR.

The problem (3) suffers is the same as this PR - I'm not sure how well the command args play with the flag args - as in:

fga model test --store-id=abcde --debug ./*.fga.yaml

What happens here? A user's expectation I think will be that it runs the tests on all the fga.yaml files in the directory - but not sure that's what will happen (prob they'll be taken as the args to the verbose flag?). We'll need to look into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

model test --tests *.fga.yaml does not work

4 participants