github.com/rainforestapp/rainforest-cli@v2.12.0+incompatible/reporter.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  
    11  	"errors"
    12  
    13  	"github.com/rainforestapp/rainforest-cli/rainforest"
    14  	"github.com/urfave/cli"
    15  )
    16  
    17  // Maximum concurrency for multithreaded HTTP requests
    18  const reporterConcurrency = 4
    19  
    20  // resourceAPI is part of the API connected to available resources
    21  type reporterAPI interface {
    22  	GetRunTestDetails(int, int) (*rainforest.RunTestDetails, error)
    23  	GetRunDetails(int) (*rainforest.RunDetails, error)
    24  }
    25  
    26  type reporter struct {
    27  	getRunDetails           func(int, reporterAPI) (*rainforest.RunDetails, error)
    28  	createOutputFile        func(string) (*os.File, error)
    29  	createJUnitReportSchema func(*rainforest.RunDetails, reporterAPI) (*jUnitReportSchema, error)
    30  	writeJUnitReport        func(*jUnitReportSchema, *os.File) error
    31  }
    32  
    33  func createReport(c cliContext) error {
    34  	r := newReporter()
    35  	return r.createReport(c)
    36  }
    37  
    38  func postRunJUnitReport(c cliContext, runID int) error {
    39  	// Get the csv file path either and skip uploading if it's not present
    40  	fileName := c.String("junit-file")
    41  	if fileName == "" {
    42  		return nil
    43  	}
    44  
    45  	r := newReporter()
    46  	return r.createJUnitReport(runID, fileName)
    47  }
    48  
    49  func newReporter() *reporter {
    50  	return &reporter{
    51  		getRunDetails:           getRunDetails,
    52  		createOutputFile:        os.Create,
    53  		createJUnitReportSchema: createJUnitReportSchema,
    54  		writeJUnitReport:        writeJUnitReport,
    55  	}
    56  }
    57  
    58  func (r *reporter) createReport(c cliContext) error {
    59  	var runID int
    60  	var err error
    61  
    62  	if runIDArg := c.Args().Get(0); runIDArg != "" {
    63  		runID, err = strconv.Atoi(runIDArg)
    64  		if err != nil {
    65  			return cli.NewExitError(err.Error(), 1)
    66  		}
    67  	} else if deprecatedRunIDArg := c.String("run-id"); deprecatedRunIDArg != "" {
    68  		runID, err = strconv.Atoi(deprecatedRunIDArg)
    69  		if err != nil {
    70  			return cli.NewExitError(err.Error(), 1)
    71  		}
    72  
    73  		log.Println("Warning: `run-id` flag is deprecated. Please provide Run ID as an argument.")
    74  	} else {
    75  		return cli.NewExitError("No run ID argument found.", 1)
    76  	}
    77  
    78  	if junitFile := c.String("junit-file"); junitFile != "" {
    79  		err = r.createJUnitReport(runID, junitFile)
    80  		if err != nil {
    81  			return cli.NewExitError(err.Error(), 1)
    82  		}
    83  	} else {
    84  		return cli.NewExitError("Output file not specified", 1)
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  func (r *reporter) createJUnitReport(runID int, junitFile string) error {
    91  	log.Print("Creating JUnit report for run #" + strconv.Itoa(runID) + ": " + junitFile)
    92  
    93  	if filepath.Ext(junitFile) != ".xml" {
    94  		return errors.New("JUnit file extension must be .xml")
    95  	}
    96  
    97  	filepath, err := filepath.Abs(junitFile)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	var runDetails *rainforest.RunDetails
   103  	runDetails, err = r.getRunDetails(runID, api)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	var outputFile *os.File
   109  	outputFile, err = r.createOutputFile(filepath)
   110  	defer outputFile.Close()
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	var reportSchema *jUnitReportSchema
   116  	reportSchema, err = r.createJUnitReportSchema(runDetails, api)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	err = r.writeJUnitReport(reportSchema, outputFile)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  func getRunDetails(runID int, client reporterAPI) (*rainforest.RunDetails, error) {
   130  	var runDetails *rainforest.RunDetails
   131  	var err error
   132  
   133  	log.Printf("Fetching details for run #" + strconv.Itoa(runID))
   134  	if runDetails, err = client.GetRunDetails(runID); err != nil {
   135  		return runDetails, err
   136  	}
   137  
   138  	if !runDetails.StateDetails.IsFinalState {
   139  		err = errors.New("Report cannot be created for an incomplete run")
   140  	}
   141  
   142  	return runDetails, err
   143  }
   144  
   145  type jUnitTestReportFailure struct {
   146  	XMLName xml.Name `xml:"failure"`
   147  	Type    string   `xml:"type,attr"`
   148  	Message string   `xml:"message,attr"`
   149  }
   150  
   151  type jUnitTestReportSchema struct {
   152  	XMLName  xml.Name `xml:"testcase"`
   153  	Name     string   `xml:"name,attr"`
   154  	Time     float64  `xml:"time,attr"`
   155  	Failures []jUnitTestReportFailure
   156  }
   157  
   158  type jUnitReportSchema struct {
   159  	XMLName   xml.Name `xml:"testsuite"`
   160  	Name      string   `xml:"name,attr"`
   161  	Tests     int      `xml:"tests,attr"`
   162  	Errors    int      `xml:"errors,attr"`
   163  	Failures  int      `xml:"failures,attr"`
   164  	Time      float64  `xml:"time,attr"`
   165  	TestCases []jUnitTestReportSchema
   166  }
   167  
   168  func createJUnitReportSchema(runDetails *rainforest.RunDetails, api reporterAPI) (*jUnitReportSchema, error) {
   169  	type processedTestCase struct {
   170  		TestCase jUnitTestReportSchema
   171  		Error    error
   172  	}
   173  
   174  	// Create channels for work to be done and results
   175  	testCount := len(runDetails.Tests)
   176  	processedTestChan := make(chan processedTestCase, testCount)
   177  	testsChan := make(chan rainforest.RunTestDetails, testCount)
   178  
   179  	processTestWorker := func(testsChan <-chan rainforest.RunTestDetails) {
   180  		for test := range testsChan {
   181  			testCase := jUnitTestReportSchema{}
   182  
   183  			testDuration := test.UpdatedAt.Sub(test.CreatedAt).Seconds()
   184  
   185  			testCase.Name = test.Title
   186  			testCase.Time = testDuration
   187  
   188  			if test.Result == "failed" {
   189  				log.Printf("Fetching information for failed test #" + strconv.Itoa(test.ID))
   190  				testDetails, err := api.GetRunTestDetails(runDetails.ID, test.ID)
   191  
   192  				if err != nil {
   193  					processedTestChan <- processedTestCase{TestCase: jUnitTestReportSchema{}, Error: err}
   194  					return
   195  				}
   196  
   197  				for _, step := range testDetails.Steps {
   198  					for _, browser := range step.Browsers {
   199  						for _, feedback := range browser.Feedback {
   200  							if feedback.Result != "failed" || feedback.JobState != "approved" {
   201  								continue
   202  							}
   203  
   204  							if feedback.FailureNote != "" {
   205  								reportFailure := jUnitTestReportFailure{Type: browser.Name, Message: feedback.FailureNote}
   206  								testCase.Failures = append(testCase.Failures, reportFailure)
   207  							} else if feedback.CommentReason != "" {
   208  								// The step failed due to a special comment type being selected
   209  								message := feedback.CommentReason
   210  
   211  								if feedback.Comment != "" {
   212  									message += ": " + feedback.Comment
   213  								}
   214  								reportFailure := jUnitTestReportFailure{Type: browser.Name, Message: message}
   215  								testCase.Failures = append(testCase.Failures, reportFailure)
   216  							}
   217  						}
   218  					}
   219  				}
   220  			}
   221  
   222  			processedTestChan <- processedTestCase{TestCase: testCase}
   223  		}
   224  	}
   225  
   226  	// spawn workers
   227  	for i := 0; i < reporterConcurrency; i++ {
   228  		go processTestWorker(testsChan)
   229  	}
   230  
   231  	// give them work
   232  	for _, test := range runDetails.Tests {
   233  		testsChan <- test
   234  	}
   235  	close(testsChan)
   236  
   237  	// and collect the results
   238  	testCases := make([]jUnitTestReportSchema, testCount)
   239  	for i := 0; i < testCount; i++ {
   240  		processed := <-processedTestChan
   241  
   242  		if processed.Error != nil {
   243  			return &jUnitReportSchema{}, processed.Error
   244  		}
   245  
   246  		testCases[i] = processed.TestCase
   247  	}
   248  
   249  	finalStateName := runDetails.StateDetails.Name
   250  	runDuration := runDetails.Timestamps[finalStateName].Sub(runDetails.Timestamps["created_at"]).Seconds()
   251  	report := &jUnitReportSchema{
   252  		Errors:    runDetails.TotalNoResultTests,
   253  		Failures:  runDetails.TotalFailedTests,
   254  		Tests:     runDetails.TotalTests,
   255  		TestCases: testCases,
   256  		Time:      runDuration,
   257  	}
   258  
   259  	if runDesc := runDetails.Description; runDesc != "" {
   260  		report.Name = runDesc
   261  	} else {
   262  		report.Name = fmt.Sprintf("Run #%v", runDetails.ID)
   263  	}
   264  
   265  	return report, nil
   266  }
   267  
   268  func writeJUnitReport(reportSchema *jUnitReportSchema, file *os.File) error {
   269  	enc := xml.NewEncoder(file)
   270  
   271  	file.Write([]byte(xml.Header))
   272  
   273  	enc.Indent("", "  ")
   274  	err := enc.Encode(reportSchema)
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	log.Printf("JUnit report successfully written to %v", file.Name())
   280  	return nil
   281  }