@@ -691,6 +691,176 @@ func TestMultilineTag(t *testing.T) {
691691 }
692692}
693693
694+ // TestHelpNoCompile verifies that -h works without compilation by using a
695+ // fixture whose function bodies reference an undefined package. The AST parser
696+ // can still extract targets, but go build would fail.
697+ func TestHelpNoCompile (t * testing.T ) {
698+ for _ , tc := range []struct {
699+ name string
700+ target string
701+ output string
702+ code int
703+ stderr bool
704+ }{
705+ {
706+ name : "known target" ,
707+ target : "build" ,
708+ output : "Build compiles the project.\n \n Usage:\n \n \t mage build\n \n " ,
709+ code : 0 ,
710+ },
711+ {
712+ name : "multiline comment" ,
713+ target : "deploy" ,
714+ output : "Deploy pushes to production. This is the extended description.\n \n Usage:\n \n \t mage deploy\n \n " ,
715+ code : 0 ,
716+ },
717+ {
718+ name : "namespace target" ,
719+ target : "ns:run" ,
720+ output : "Run runs within the namespace.\n \n Usage:\n \n \t mage ns:run\n \n " ,
721+ code : 0 ,
722+ },
723+ {
724+ name : "unknown target" ,
725+ target : "doesnotexist" ,
726+ output : "Unknown target: \" doesnotexist\" \n " ,
727+ code : 2 ,
728+ stderr : true ,
729+ },
730+ } {
731+ t .Run (tc .name , func (t * testing.T ) {
732+ stdout := & bytes.Buffer {}
733+ stderr := & bytes.Buffer {}
734+ inv := Invocation {
735+ Dir : "testdata/help_no_compile" ,
736+ Stdout : stdout ,
737+ Stderr : stderr ,
738+ Help : true ,
739+ Args : []string {tc .target },
740+ }
741+ code := Invoke (inv )
742+ if code != tc .code {
743+ t .Fatalf ("expected exit code %d, got %d\n stdout: %s\n stderr: %s" , tc .code , code , stdout , stderr )
744+ }
745+ var got string
746+ if tc .stderr {
747+ got = stderr .String ()
748+ } else {
749+ got = stdout .String ()
750+ }
751+ if got != tc .output {
752+ t .Errorf ("expected output %q, got %q" , tc .output , got )
753+ }
754+ })
755+ }
756+ }
757+
758+ // TestHelpNoTarget verifies that -h without a target name prints an error.
759+ func TestHelpNoTarget (t * testing.T ) {
760+ stdout := & bytes.Buffer {}
761+ stderr := & bytes.Buffer {}
762+ inv := Invocation {
763+ Dir : "testdata/multiline" ,
764+ Stdout : stdout ,
765+ Stderr : stderr ,
766+ Help : true ,
767+ Args : []string {},
768+ }
769+ code := Invoke (inv )
770+ if code != 2 {
771+ t .Fatalf ("expected exit code 2, got %d\n stdout: %s\n stderr: %s" , code , stdout , stderr )
772+ }
773+ got := stderr .String ()
774+ want := "no target specified\n "
775+ if got != want {
776+ t .Errorf ("expected %q, got %q" , want , got )
777+ }
778+ }
779+
780+ // TestHelpAliases verifies that -h shows sorted aliases.
781+ func TestHelpAliases (t * testing.T ) {
782+ stdout := & bytes.Buffer {}
783+ stderr := & bytes.Buffer {}
784+ inv := Invocation {
785+ Dir : "testdata/alias" ,
786+ Stdout : stdout ,
787+ Stderr : stderr ,
788+ Help : true ,
789+ Args : []string {"status" },
790+ }
791+ code := Invoke (inv )
792+ if code != 0 {
793+ t .Fatalf ("expected exit code 0, got %d\n stdout: %s\n stderr: %s" , code , stdout , stderr )
794+ }
795+ got := stdout .String ()
796+ want := "Prints status.\n \n Usage:\n \n \t mage status\n \n Aliases: st, stat\n \n "
797+ if got != want {
798+ t .Errorf ("expected %q, got %q" , want , got )
799+ }
800+ }
801+
802+ // TestHelpMatchesCompiledBinary verifies that mage -h and a compiled binary's
803+ // -h produce identical output for the same target.
804+ func TestHelpMatchesCompiledBinary (t * testing.T ) {
805+ dir := "./testdata/compiled"
806+ compileDir := t .TempDir ()
807+ name := filepath .Join (compileDir , "mage_help_test" )
808+ if runtime .GOOS == "windows" {
809+ name += ".exe"
810+ }
811+
812+ // Compile the binary.
813+ stderr := & bytes.Buffer {}
814+ inv := Invocation {
815+ Dir : dir ,
816+ Stdout : io .Discard ,
817+ Stderr : stderr ,
818+ CompileOut : name ,
819+ }
820+ code := Invoke (inv )
821+ if code != 0 {
822+ t .Fatalf ("compile failed with code %d: %s" , code , stderr )
823+ }
824+
825+ // Get help from mage directly (no compilation path).
826+ stdout := & bytes.Buffer {}
827+ stderr .Reset ()
828+ inv = Invocation {
829+ Dir : dir ,
830+ Stdout : stdout ,
831+ Stderr : stderr ,
832+ Help : true ,
833+ Args : []string {"deploy" },
834+ }
835+ code = Invoke (inv )
836+ if code != 0 {
837+ t .Fatalf ("mage -h deploy failed with code %d: %s" , code , stderr )
838+ }
839+ mageOutput := stdout .String ()
840+
841+ // Get help from the compiled binary.
842+ stdout .Reset ()
843+ stderr .Reset ()
844+ cmd := exec .CommandContext (context .Background (), name , "-h" , "deploy" )
845+ cmd .Env = os .Environ ()
846+ cmd .Stdout = stdout
847+ cmd .Stderr = stderr
848+ if err := cmd .Run (); err != nil {
849+ t .Fatalf ("compiled binary -h deploy failed: %v\n stderr: %s" , err , stderr )
850+ }
851+ compiledOutput := stdout .String ()
852+
853+ // The binary name differs (compiled binary uses its own filename), so
854+ // normalize both outputs by replacing the binary name with a placeholder.
855+ binaryBase := filepath .Base (name )
856+ normalizedMage := strings .ReplaceAll (mageOutput , "\t mage " , "\t BINARY " )
857+ normalizedCompiled := strings .ReplaceAll (compiledOutput , "\t " + binaryBase + " " , "\t BINARY " )
858+
859+ if normalizedMage != normalizedCompiled {
860+ t .Errorf ("help output mismatch (after normalizing binary name):\n mage -h: %q\n compiled -h: %q" , mageOutput , compiledOutput )
861+ }
862+ }
863+
694864func TestList (t * testing.T ) {
695865 stdout := & bytes.Buffer {}
696866 inv := Invocation {
0 commit comments