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

magefile: added combined test coverage support for mage test:all #2235

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

kartikaysaxena
Copy link
Contributor

Fixes #1348 mage test:all now generates combined coverprofile aggregated into coverage.txt

Signed-off-by: Kartikay <[email protected]>
@kartikaysaxena kartikaysaxena requested a review from a team as a code owner February 4, 2025 21:19
testArgs := append([]string{
"test",
"-covermode=atomic",
"-coverprofile=coverage.txt",
Copy link
Member

Choose a reason for hiding this comment

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

Won't this overwrite the coverage.txt file when each individual test suite runs? Can you try running mage test:all and ensure it combines the coverage?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed the approach, now each test writes to a different file and removes those files in the end, go tool cover -html=coverage.txt now shows
Screenshot 2025-02-06 at 12 25 43 AM

@@ -210,3 +223,45 @@ func run(dir string, env map[string]string, stdout, stderr io.Writer, cmd string
err = c.Run()
return sh.CmdRan(err), sh.ExitStatus(err), err
}

func sanitizePath(path string) string {
Copy link
Member

Choose a reason for hiding this comment

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

I'd recommend hashing the path here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

@vroldanbet vroldanbet left a comment

Choose a reason for hiding this comment

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

Seems like the PR enables coverage by default on all, whereas before it was opt in. Do we have concerns that would add overhead on the inner loop? Or is the overhead negligible?

@kartikaysaxena kartikaysaxena force-pushed the mage-test-coverage branch 2 times, most recently from 4fb47db to 7498752 Compare February 10, 2025 22:12
@kartikaysaxena
Copy link
Contributor Author

Seems like the PR enables coverage by default on all, whereas before it was opt in. Do we have concerns that would add overhead on the inner loop? Or is the overhead negligible?

I used time mage test:all to check and the overhead was costing over a minute, followed the pattern for other targets for generating coverage reports which was earlier in UnitCover, now we have a dedicated test:allcover for this, is this correct? Would love the feedback and any improvements further if necessary

@@ -210,3 +221,40 @@ func run(dir string, env map[string]string, stdout, stderr io.Writer, cmd string
err = c.Run()
return sh.CmdRan(err), sh.ExitStatus(err), err
}

func hashPath(path string) string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to hash the path? I'd keep it simpler by generating a UUID as suffix

func (t Test) AllCover() error {
ds := Testds{}
c := Testcons{}
mg.Deps(t.UnitCover, t.IntegrationCover, t.SteelthreadCover, t.ImageCover, t.AnalyzersCover,
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems like a lot of duplication that would lead to drift w.r.t to All() method. Ideally All and AllCover invoke `all(coverage bool)

return err
}

for _, file := range files {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why deleting the tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

was for deleting individual coverage files after merging them. reverting

"-tags", "ci,docker,datastoreconsistency",
"-timeout", "10m",
"-run", fmt.Sprintf("TestConsistencyPerDatastore/%s", datastore))
if coverage {
Copy link
Contributor

Choose a reason for hiding this comment

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

all this if coverage blocks could be refactored to avoid all the duplication - e.g. a function that takes the args and the coverage bool, and returns the args with the appended parameters.


func (Testds) spanner(coverage bool) error {
args := []string{"-tags", "ci,docker", "-timeout", "10m"}
if coverage {
Copy link
Contributor

Choose a reason for hiding this comment

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

lots of duplication: this coverage if statement should be handled inside datastoreTest

func (Test) image(coverage bool) error {
mg.Deps(Build{}.Testimage)
args := []string{"-tags", "docker,image"}
if coverage {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be moved into goDirTest, otherwise it leads to a lot of duplication

@@ -43,27 +53,75 @@ func (Test) unit(coverage bool) error {
fmt.Println("running unit tests")
args := []string{"-tags", "ci,skipintegrationtests", "-race", "-timeout", "10m", "-count=1"}
if coverage {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be moved into goTest, otherwise it leads to a lot of duplication

}

// SteelthreadCover Runs the steelthread tests and generates a coverage report
func (t Test) SteelthreadCover() error {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's annoying we end up with proliferation of commands just to have with/without coverage versions of them. Calling mage leads to a very long list that starts to become not very user friendly.

I know mage supports arguments in commands, but not if it supports commands with arguments and default values, could you investigate?

Signed-off-by: Kartikay <[email protected]>
@kartikaysaxena
Copy link
Contributor Author

Utilised uuid instead, also passed the bool through a context to goDirTest and goDirTestWithEnv, now works like mage test:all -cover=true

Comment on lines 45 to 58
testArgs := append([]string{
"test",
"-failfast",
"-count=1",
}, args...)
if cover, _ := ctx.Value("cover").(bool); cover {
if err := os.MkdirAll("coverage", 0o755); err != nil {
return fmt.Errorf("failed to create coverage directory: %w", err)
}
testArgs = append(testArgs, []string{
"-covermode=atomic",
fmt.Sprintf("-coverprofile=coverage-%s.txt", uuid.New().String()),
}...)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please refactor this logic into a single method that is used in both goDirTestWithEnv and goDirTest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done with testWithArgs

"-failfast",
"-count=1",
}, args...)
if cover, _ := ctx.Value("cover").(bool); cover {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: would you mind replacing this with https://github.com/authzed/ctxkey 🙏🏻

"-count=1",
}, args...)
if cover, _ := ctx.Value("cover").(bool); cover {
if err := os.MkdirAll("coverage", 0o755); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to give permissions to group and public? I think the owner having access is enough

Comment on lines 23 to 31
ctx := context.Background()
cover := false
for _, arg := range os.Args {
if arg == "-cover=true" {
cover = true
break
}
}
ctx = context.WithValue(ctx, "cover", cover)
Copy link
Contributor

Choose a reason for hiding this comment

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

Using the context to propagate arguments is a good idea to workaround mage parameterization limitations 👍🏻

Can you refactor this into a function in case we need to add more flags for other commands? I also suggest using https://github.com/authzed/ctxkey for type-safe context values

return nil
}

func (t Test) Combine() error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is combining everything something that has to be invoked explicitly? Could we call this from All if the cover flag was enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Signed-off-by: Kartikay <[email protected]>
kartikaysaxena and others added 3 commits February 26, 2025 12:49
Signed-off-by: Kartikay <[email protected]>
Signed-off-by: Kartikay <[email protected]>
Copy link
Contributor

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

See comment; once that's through it looks good to me.

Comment on lines 26 to 27
ctxMyKey := ctxkey.New[bool]()
ctx := ctxMyKey.WithValue(context.Background(), cover)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it work to declare ctxMyKey twice? I'd expect that that would give you two different identities of keys, which wouldn't allow you to pull values out of context. The idea is that you'd give the key a name that means something and declare it once at the top of the file. There's an example in the ctxkey examples dir.

Would you please declare this key once at the top of the module with a meaningful name and then reuse it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

was doing that to avoid a global var, seems like it should be the way to go for this.

@@ -19,51 +22,56 @@ var emptyEnv map[string]string
func (t Test) All() error {
ds := Testds{}
c := Testcons{}
mg.Deps(t.Unit, t.Integration, t.Steelthread, t.Image, t.Analyzers,
cover := parseCommandLineFlags()
ctxMyKey := ctxkey.New[bool]()
Copy link
Contributor

@vroldanbet vroldanbet Feb 27, 2025

Choose a reason for hiding this comment

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

This should be defined as a global var, a key to a specific value.
Also you could add context.Context to All signature, mage supports it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

"-failfast",
"-count=1",
}, args...)
ctxMyKey := ctxkey.New[bool]()
Copy link
Contributor

Choose a reason for hiding this comment

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

this should reference the global var

@github-actions github-actions bot added the area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) label Feb 27, 2025
ds := Testds{}
c := Testcons{}
cover := parseCommandLineFlags()
ctxMyKey := ctxkey.New[bool]()
ctx := ctxMyKey.WithValue(context.Background(), cover)
ctx = ctxMyKey.WithValue(context.Background(), cover)
Copy link
Contributor

Choose a reason for hiding this comment

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

use the ctx argument provided by the method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah overlooked that, thanks for pointing out!

Comment on lines 48 to 49
cover, err := ctxMyKey.Value(ctx)
if cover && err {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not idiomatic, because the second argument is not of error type, and that confuses the reader thinking it is an error. Also the if expression reads weird because it says if coverage is enabled and an error happened....

Methods that return value and a ok boolean are typically used like this:

if cover, ok := ctxMyKey.Value(ctx); cover && ok {

}

Signed-off-by: Kartikay <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

mage test:all should produce a combined code coverage result
4 participants