github.com/thediveo/gons@v0.9.9/reexec/testing/m.go (about)

     1  // Copyright 2020 Harald Albrecht.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package testing
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	gotesting "testing"
    21  
    22  	"github.com/thediveo/gons/reexec/internal/testsupport"
    23  )
    24  
    25  // M is an "enhanced" version of Golang's testing.M which additionally handles
    26  // merging coverage profile data from re-executions into the main ("parent's")
    27  // coverage profile file.
    28  type M struct {
    29  	*gotesting.M
    30  	skipCleanup bool
    31  }
    32  
    33  // Run runs the tests and for the parent process then correctly merges the
    34  // coverage profile data from re-executed process copies into this parent
    35  // process' coverage profile data. Run returns an exit code to pass to
    36  // os.Exit.
    37  func (m *M) Run() (exitcode int) {
    38  	exitcode, _ = m.run()
    39  	return
    40  }
    41  
    42  // run is the internal implementation of the public Run() method, and
    43  // additionally returns an indication of whether we were running as the parent
    44  // process or a re-executed child process. This indication is used by
    45  // TestMainWithCoverage() to correctly update coverage data to also include
    46  // almost complete coverage of our M.run() code.
    47  func (m *M) run() (exitcode int, reexeced bool) {
    48  	// If necessary, run the action first, as this gathers the coverage
    49  	// profile data during re-execution, which we are interested in. Please
    50  	// note that we cannot use gons.reexec.RunAction() directly, as this would
    51  	// result in an import cycle. To break this vicious cycle we use
    52  	// testsupport's RunAction instead, which gons.reexec will initialize to
    53  	// point to its real implementation of RunAction.
    54  	var recovered interface{}
    55  	func() {
    56  		// RunAction() panics when it is asked to run a non-registered action.
    57  		// But we still want to write coverage profile data, so we need to
    58  		// wrap the call to RunAction(), so that we can recover.
    59  		defer func() {
    60  			if recovered = recover(); recovered != nil {
    61  				// RunAction panics only when trying to re-execute, never
    62  				// otherwise.
    63  				reexeced = true
    64  			}
    65  		}()
    66  		reexeced = testsupport.RunAction()
    67  	}()
    68  	// If we're in coverage mode and we're the parent test process, then pass
    69  	// the required test argument settings to the gons/reexec package, so that
    70  	// it can correctly re-execute child processes under test.
    71  	parseCoverageArgs(os.Args)
    72  	if !reexeced {
    73  		testsupport.EnableTesting(outputDir, coverProfile)
    74  	}
    75  	// Run the tests: for the parent this will be an ordinary test run, but
    76  	// for a re-executed child the passed "-test.run" argument will ensure
    77  	// that actually no tests are run at all, because that would result in
    78  	// tests executed multiple times and panic when hitting a recursive
    79  	// reexec.ForkReexec() call.
    80  	if !reexeced {
    81  		// testing's M.Run() will write the coverage report even when a test
    82  		// panics. And since tests might have used reexec.ForkReexec() we
    83  		// should merge any child coverage profile data results with what
    84  		// M.Run() reported.
    85  		func() {
    86  			defer func() {
    87  				recovered = recover()
    88  			}()
    89  			exitcode = m.M.Run()
    90  		}()
    91  		// For the parent we finally need to gather the coverage profile data
    92  		// written by the individual re-executed child processes, and merge it
    93  		// with our own coverage profile data. Our data has been written at the
    94  		// end of the (empty) m.M.Run(), so we can only now do the final merge.
    95  		if coverProfile != "" && exitcode == 0 {
    96  			mergeAndReportCoverages(coverProfile, testsupport.CoverageProfiles)
    97  			// Now clean up!
    98  			if !m.skipCleanup {
    99  				for _, coverprof := range testsupport.CoverageProfiles {
   100  					_ = os.Remove(toOutputDir(coverprof))
   101  				}
   102  			}
   103  		}
   104  		if recovered != nil {
   105  			// Recover panic!!!
   106  			panic(recovered)
   107  		}
   108  	} else {
   109  		// Run the empty test set when we're an re-executed child, so that the
   110  		// Go testing package creates a coverage profile data report.
   111  		pritiPratel(func() {
   112  			exitcode = m.M.Run()
   113  		})
   114  		// If RunAction() panicked, we "recover our panic", but this way the
   115  		// coverage data has been generated and can later be merged.
   116  		if recovered != nil {
   117  			fmt.Fprint(os.Stderr, recovered)
   118  		}
   119  	}
   120  	return
   121  }