Skip to content

Streamline tests with a custom iterator#88

Merged
MusicalNinjaDad merged 70 commits into
mainfrom
test_iterator
Nov 3, 2025
Merged

Streamline tests with a custom iterator#88
MusicalNinjaDad merged 70 commits into
mainfrom
test_iterator

Conversation

@MusicalNinjaDad

@MusicalNinjaDad MusicalNinjaDad commented Nov 3, 2025

Copy link
Copy Markdown
Owner

Summary by Sourcery

Refactor test infrastructure by introducing a custom iterator and unified Asserter API, migrating all test suites to a table-driven pattern and removing redundant helper functions.

New Features:

  • Introduce TestCases iterator to streamline table-driven tests
  • Add Asserter helper with DirectoryContents, LinkedFile, and Stdout assertions

Enhancements:

  • Refactor existing tests to use TestDetails and TestCase structs with TestCases iterator
  • Consolidate expected output generation and test data into internal testing package
  • Remove legacy assertion helpers and duplicate test code across test suites

Build:

  • Add github.com/ameghdadian/x/iter and github.com/davecgh/go-spew dependencies in go.mod

@sourcery-ai

sourcery-ai Bot commented Nov 3, 2025

Copy link
Copy Markdown

Reviewer's Guide

This PR overhauls the test infrastructure by introducing a custom TestCases iterator alongside a centralized Asserter helper, refactors existing table-driven tests to consume that iterator, removes legacy testing helpers, and cleans up comparison utilities. A new iter dependency is added to orchestrate subtests more declaratively, streamlining repetitive loops.

Class diagram for new and updated test helpers

classDiagram
    class Asserter {
        - Testify *assert.Assertions
        - t *testing.T
        + DirectoryContents(ExpectedContents map[string]string, dir string)
        + LinkedFile(path1 string, path2 string)
        + Stdout(expected []string, actual []string, mustBeLinked ...string)
    }
    class TestDetails {
        + Name string
        + Path string
        + Bin binaryDetails
        + SnagTo string
        + SnagAs string
    }
    class TestCase {
        + Src string
        + Dest string
        + ExpectedStdout []string
        + ExpectedFiles map[string]string
        + ExpectedBinary binaryDetails
        + Options []snaggle.Option
        + Flags []string
    }
    class binaryDetails {
        + Description string
        + Elf elf.Elf
        + Dynamic bool
        + Exe bool
        + Lib bool
        + HasInterpreter bool
    }
    Asserter --> assert.Assertions
    Asserter --> testing.T
    TestDetails --> binaryDetails
    TestCase --> binaryDetails
    TestCase --> snaggle.Option
Loading

Class diagram for refactored comparison utilities

classDiagram
    class SameInode {
        + SameInode(path1 string, path2 string) (bool, error)
    }
    class SameFile {
        + SameFile(path1 string, path2 string) bool
    }
    SameFile --> SameInode
Loading

Flow diagram for the new TestCases iterator usage

flowchart TD
    A["TestCases(t, tests...)"] --> B["Iterate over inplace: false/true"]
    B --> C["Iterate over each TestDetails"]
    C --> D["t.Run(desc, func(t *testing.T) {...})"]
    D --> E["Create TestCase"]
    E --> F["generateOutput(bin, &tc, inplace)"]
    F --> G["testbody(t, tc)"]
    B --> H["If not specific tests, run directory tests (recursive/non-recursive)"]
    H --> I["t.Run(desc, func(t *testing.T) {...})"]
    I --> J["Create TestCase for directory"]
    J --> K["For each bin, generateOutput"]
    K --> L["testbody(t, tc)"]
Loading

File-Level Changes

Change Details Files
Introduce custom iterator for table-driven tests
  • Add new internal/testing/testiterator.go
  • Define TestCases, TestCase struct, defaultTests, and generateOutput
  • Leverage iter.Seq2 to run subtests over inplace and recursive modes
internal/testing/testiterator.go
Centralize assertion logic into Asserter
  • Add internal/testing/assertions.go with Assert(t) returning an Asserter
  • Implement DirectoryContents, LinkedFile, and Stdout methods with testify under the hood
internal/testing/assertions.go
Refactor tests to use TestCases and Asserter
  • Replace manual loops in snaggle_test.go and cli_test.go with TestCases-based Test(t,tc)
  • Update elf/elf_test.go to iterate GoodElfs via TestCases
  • Remove maps/slices imports and legacy CommonBinaries/ExpectedOutput calls
  • Switch assertions to use Assert(t).Testify methods
snaggle_test.go
cmd/snaggle/cli_test.go
elf/elf_test.go
internal/testing/testdata.go
Remove legacy helpers and update comparison logic
  • Rename sameInode to public SameInode
  • Delete AssertSameFile, AssertLinkedFile, and AssertDirectoryContents from internal/comparisons.go
internal/comparisons.go
Add iter library dependency
  • Add github.com/ameghdadian/x/iter to go.mod
  • Introduce github.com/davecgh/go-spew for debugging test cases
go.mod

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Nov 3, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.45763% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.36%. Comparing base (7027441) to head (04e6eb7).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/testing/assertions.go 92.30% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #88      +/-   ##
==========================================
- Coverage   74.56%   74.36%   -0.20%     
==========================================
  Files          11       12       +1     
  Lines         578      593      +15     
==========================================
+ Hits          431      441      +10     
- Misses        104      108       +4     
- Partials       43       44       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `internal/testing/testdata.go:21` </location>
<code_context>
 }

-var commonElfs = map[string]elf.Elf{
+var GoodElfs = map[string]binaryDetails{
 	"hello_pie": {
-		Name:         "hello_pie",
</code_context>

<issue_to_address>
**issue (complexity):** Consider introducing a helper function to automatically set binaryDetails flags based on the Elf struct, reducing manual repetition.

You can pull the repeated flag‐logic into a single helper and then just pass your Elf literal plus a description. For example:

```go
func newBinaryDetails(desc string, e elf.Elf) binaryDetails {
    return binaryDetails{
        Description:    desc,
        Elf:            e,
        Dynamic:        e.Type == elf.Type(elf.PIE) || e.Type == elf.Type(elf.DYN),
        Exe:            e.Type == elf.Type(elf.PIE) || e.Type == elf.Type(elf.EXE),
        Lib:            e.Type == elf.Type(elf.DYN),
        HasInterpreter: e.Interpreter != "",
    }
}
```

Then your map becomes much shorter:

```go
var GoodElfs = map[string]binaryDetails{
    "hello_pie":    newBinaryDetails("PIE no dependencies",    elf.Elf{ Name: "hello_pie", Path: P_hello_pie, Class: elf.EI_CLASS(elf.ELF64), Type: elf.Type(elf.PIE), Interpreter: P_ld_linux, Dependencies: nil }),
    "hello_static": newBinaryDetails("Static linked executable", elf.Elf{ Name: "hello_static", Path: P_hello_static, Class: elf.EI_CLASS(elf.ELF64), Type: elf.Type(elf.EXE), Interpreter: "", Dependencies: nil }),
    // …and so on for the other entries…
}

var Ldd = newBinaryDetails("Not an ELF", elf.Elf{
    Name: "ldd", Path: P_ldd,
    Class: elf.EI_CLASS(elf.UNDEF), Type: elf.Type(elf.ELFNONE),
    Interpreter: "", Dependencies: nil,
})
```

This preserves exactly the same behavior while removing all of the manual flag repetitions.
</issue_to_address>

### Comment 2
<location> `internal/testing/testiterator.go:84` </location>
<code_context>
+//
+// Just don't try to use SkipNow, FailNow or t.Parallel as Go's rangefunc & t.Run hacks collide and mess up
+// goroutine clarity. There is probably a good fix with channels but I can't be bothered right now...
+func TestCases(t *testing.T, tests ...TestDetails) iter.Seq2[*testing.T, TestCase] {
+	var specficTestsRequested bool
+	if tests == nil {
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring TestCases to return a slice of TestCase and use a simple for loop for subtests.

Here’s one way to collapse all that indirection into a simple slice + single `t.Run(…)` loop without losing any of the test-matrix:

1. Change `TestCases` to build and return `[]TestCase` instead of an `iter.Seq2`:

```go
// returns all permutations of TestCase (in-place, recursive, directory, etc)
func TestCases(t *testing.T, tests ...TestDetails) []TestCase {
    base := defaultTests
    if len(tests) > 0 {
        base = tests
    }

    var out []TestCase
    for _, inplace := range []bool{false, true} {
        for _, td := range base {
            name := td.Name
            if inplace {
                name += "_inplace"
            }
            tc := buildSingleCase(t, td, inplace)
            tc.Name = name
            out = append(out, tc)
        }

        // only for default set, also test directory runs
        if len(tests) == 0 {
            for _, recursive := range []bool{false, true} {
                name := "Directory"
                if inplace {
                    name += "_inplace"
                }
                if recursive {
                    name += "_recursive"
                }
                tc := buildDirCase(t, defaultTests, inplace, recursive)
                tc.Name = name
                out = append(out, tc)
            }
        }
    }
    return out
}
```

2. Implement the two helpers to keep things DRY:

```go
func buildSingleCase(t *testing.T, td TestDetails, inplace bool) TestCase {
    tc := TestCase{ /* initialize fields */ }
    generateOutput(td, &tc, inplace)
    return tc
}

func buildDirCase(t *testing.T, bins []TestDetails, inplace, recursive bool) TestCase {
    tc := TestCase{ /* dst/src/options based on flags */ }
    for _, td := range bins {
        generateOutput(td, &tc, inplace)
    }
    return tc
}
```

3. In your test, consume that slice directly:

```go
func TestSomething(t *testing.T) {
    for _, tc := range TestCases(t) {
        t.Run(tc.Name, func(t *testing.T) {
            Assert := assert.New(t)
            testbody(t, tc) // whatever your body does
        })
    }
}
```

Benefits:

- No custom iterator type or higher-order function needed.
- One flat `for _, tc := range …` loop makes it immediately clear how many subtests you'll get.
- You can still reuse `generateOutput` and keep the same flags/options logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/testing/testdata.go
Comment thread internal/testing/testiterator.go
@MusicalNinjaDad MusicalNinjaDad enabled auto-merge (squash) November 3, 2025 15:23
@MusicalNinjaDad MusicalNinjaDad merged commit bc8b26f into main Nov 3, 2025
14 checks passed
@MusicalNinjaDad MusicalNinjaDad deleted the test_iterator branch November 3, 2025 15:25
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.

1 participant