report/api_test_report.go (202 lines of code) (raw):

package report import ( "encoding/json" "fmt" "os" "os/exec" "path" "path/filepath" "sort" "strings" "github.com/azure/armstrong/coverage" "github.com/azure/armstrong/utils" "github.com/sirupsen/logrus" ) const ( TestReportDirName = "ArmstrongReport" TraceLogDirName = "traces" ApiTestReportFileName = "API Test - SwaggerAccuracyReport" ApiTestConfigFileName = "ApiTestConfig.json" CoverageReportFileName = "API Test - CoverageReport.md" ) type ApiTestReport struct { CoveredSpecFiles []string `json:"coveredSpecFiles"` UnCoveredOperationsList []UnCoveredOperations `json:"unCoveredOperationsList"` Errors []ErrorItem `json:"errors"` } type UnCoveredOperations struct { Spec string `json:"spec"` OperationIds []string `json:"operationIds"` } type ErrorItem struct { Spec string `json:"spec"` ErrorCode string `json:"errorCode"` ErrorLink string `json:"errorLink"` ErrorMessage string `json:"errorMessage"` OperationId string `json:"operationId"` SchemaPathWithPosition string `json:"schemaPathWithPosition"` } type ApiTestConfig struct { SuppressionList []Suppression `json:"suppressionList"` } type Suppression struct { Code string `json:"rule"` File string `json:"file"` Operation string `json:"operation"` Reason string `json:"reason"` } func OavValidateTraffic(traceDir string, swaggerPath string, outputDir string) (*ApiTestReport, error) { htmlReportFilePath := path.Join(outputDir, fmt.Sprintf("%s.html", ApiTestReportFileName)) jsonReportFilePath := path.Join(outputDir, fmt.Sprintf("%s.json", ApiTestReportFileName)) logrus.Debugf("oav validate-traffic %s %s --report %s --jsonReport %s", traceDir, swaggerPath, htmlReportFilePath, jsonReportFilePath) cmd := exec.Command("oav", "validate-traffic", traceDir, swaggerPath, "--report", htmlReportFilePath, "--jsonReport", jsonReportFilePath) if err := cmd.Run(); err != nil { logrus.Warnf("oav validates-traffic: %+v", err) } contentBytes, err := os.ReadFile(jsonReportFilePath) if err != nil { return nil, fmt.Errorf("error when opening file(%s): %+v", jsonReportFilePath, err) } var payload *ApiTestReport err = json.Unmarshal(contentBytes, &payload) if err != nil { return nil, fmt.Errorf("error during Unmarshal() for file(%s): %+v", jsonReportFilePath, err) } if payload == nil { return nil, fmt.Errorf("oav report is empty") } // remove duplicated error items errorMap := make(map[string]ErrorItem) for _, errItem := range payload.Errors { errorMap[fmt.Sprintf("%s-%s-%s-%s", errItem.ErrorCode, errItem.ErrorMessage, errItem.OperationId, errItem.SchemaPathWithPosition)] = errItem } errors := make([]ErrorItem, 0) for _, v := range errorMap { errors = append(errors, v) } payload.Errors = errors return payload, nil } func GenerateApiTestReports(wd string, swaggerPath string) error { testReportPath := path.Join(wd, TestReportDirName) traceLogPath := path.Join(testReportPath, TraceLogDirName) swaggerPath, _ = filepath.Abs(swaggerPath) logrus.Infof("copying trace files to %s...", traceLogPath) if err := mergeApiTestTraceFiles(wd, traceLogPath); err != nil { return fmt.Errorf("[ERROR] failed to merge trace files: %+v", err) } logrus.Infof("validating traces...") report, err := OavValidateTraffic(traceLogPath, swaggerPath, testReportPath) if err != nil { return fmt.Errorf("[ERROR] failed to retrieve oav report: %+v", err) } opCovReport, err := coverage.NewOperationPropertiesCoverageReport(traceLogPath, swaggerPath) if err != nil { logrus.Warnf("[ERROR] failed to generate operation properties coverage report: %+v", err) } logrus.Infof("generating markdown report...") opConvMarkdownContent := opCovReport.MarkdownContent() if err := os.WriteFile(path.Join(testReportPath, CoverageReportFileName), []byte(opConvMarkdownContent), 0644); err != nil { logrus.Warnf("error when writing file(%s): %+v", path.Join(testReportPath, CoverageReportFileName), err) } else { logrus.Infof("markdown report saved to %s", path.Join(testReportPath, CoverageReportFileName)) } if err = generateApiTestMarkdownReport(*report, *opCovReport, swaggerPath, testReportPath, path.Join(wd, ApiTestConfigFileName)); err != nil { return fmt.Errorf("[ERROR] failed to generate markdown report: %+v", err) } return nil } func mergeApiTestTraceFiles(wd string, traceLogPath string) error { if err := os.RemoveAll(traceLogPath); err != nil { return fmt.Errorf("error removing test trace dir %s: %+v", traceLogPath, err) } if err := os.MkdirAll(traceLogPath, 0755); err != nil { return fmt.Errorf("error creating test report dir %s: %+v", traceLogPath, err) } dirs, err := os.ReadDir(wd) if err != nil { return fmt.Errorf("failed to read working directory: %+v", err) } for _, d := range dirs { if !d.IsDir() { continue } traceDir := filepath.Join(wd, d.Name(), TraceLogDirName) if utils.Exists(traceDir) && traceDir != traceLogPath { err := utils.CopyWithOptions(traceDir, traceLogPath, fmt.Sprintf("%s-", d.Name())) if err != nil { return fmt.Errorf("failed to copy trace files: %+v", err) } } } return nil } func isSuppressedInApiTest(suppressionList []Suppression, rule string, filePath string, operation string) bool { segments := strings.Split(filepath.ToSlash(filePath), "/") file := segments[len(segments)-1] for _, suppression := range suppressionList { if strings.EqualFold(suppression.Code, rule) && strings.EqualFold(suppression.File, file) && (strings.EqualFold(suppression.Code, "SWAGGER_NOT_TEST") || strings.EqualFold(suppression.Operation, operation)) { return true } } return false } func generateApiTestMarkdownReport(result ApiTestReport, opCovReport coverage.CoverageReport, swaggerPath string, testReportPath string, apiTestConfigFilePath string) error { var config ApiTestConfig if utils.Exists(apiTestConfigFilePath) { contentBytes, err := os.ReadFile(apiTestConfigFilePath) if err != nil { logrus.Errorf("error when opening file(%s): %+v", apiTestConfigFilePath, err) } err = json.Unmarshal(contentBytes, &config) if err != nil { logrus.Errorf("error during Unmarshal() for file(%s): %+v", apiTestConfigFilePath, err) } } else { logrus.Debugf("no config file found") } mdTitle := "## API TEST ERROR REPORT<br>\n|Rule|Message|\n|---|---|" mdTable := make([]string, 0) testedMap := make(map[string]bool) for _, v := range result.CoveredSpecFiles { v = strings.ReplaceAll(v, "\\", "/") testedMap[v] = true } swaggerFiles, err := utils.ListFiles(swaggerPath, ".json", 1) if err != nil { return err } for _, v := range swaggerFiles { v = strings.ReplaceAll(v, "\\", "/") if _, exists := testedMap[v]; !exists { if !isSuppressedInApiTest(config.SuppressionList, "SWAGGER_NOT_TEST", v, "") { mdTable = append(mdTable, fmt.Sprintf("|[SWAGGER_NOT_TEST](about:blank)|**message**: No operations in swagger is test.<br>**location**: %s", v[strings.Index(v, "/specification/"):])) } } } for _, operationsItem := range result.UnCoveredOperationsList { for _, id := range operationsItem.OperationIds { if !isSuppressedInApiTest(config.SuppressionList, "OPERATION_NOT_TEST", operationsItem.Spec, id) { mdTable = append(mdTable, fmt.Sprintf("|[OPERATION_NOT_TEST](about:blank)|**message**: **%s** opeartion is not test.<br>**opeartion**: %s<br>**location**: %s", id, id, operationsItem.Spec[strings.Index(operationsItem.Spec, "/specification/"):])) } } } for _, errItem := range result.Errors { location := errItem.Spec[strings.Index(errItem.Spec, "/specification/"):] normalizedPath := filepath.ToSlash(errItem.SchemaPathWithPosition) if subIndex := strings.Index(normalizedPath, "/specification/"); subIndex != -1 { location = normalizedPath[subIndex:] } if !isSuppressedInApiTest(config.SuppressionList, errItem.ErrorCode, errItem.Spec, errItem.OperationId) { mdTable = append(mdTable, fmt.Sprintf("|[%s](%s)|**message**: %s.<br>**opeartion**: %s<br>**location**: %s", errItem.ErrorCode, errItem.ErrorLink, errItem.ErrorMessage, errItem.OperationId, location)) } } sort.Strings(mdTable) mdContent := mdTitle + "\n" + strings.Join(mdTable, "\n") mdContent += opCovReport.MarkdownContentCompact() mdReportFilePath := path.Join(testReportPath, fmt.Sprintf("%s.md", ApiTestReportFileName)) if err := os.WriteFile(mdReportFilePath, []byte(mdContent), 0644); err != nil { return fmt.Errorf("error when writing file(%s): %+v", mdReportFilePath, err) } logrus.Infof("markdown report saved to %s", mdReportFilePath) return nil }