From ca943e1dc8d87126f6d0189d6ffbce57439b8ab1 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 1 Sep 2023 12:08:49 +0300 Subject: [PATCH] Support result formatting Signed-off-by: Denis Kirillov --- README.md | 2 +- cmd/parser/modules/compatibility.go | 238 +++++++++--------- internal/parser/parser.go | 91 +++++++ .../s3}/resources/tests-struct.json | 210 ++++++++-------- internal/s3/structure.go | 31 +++ .../templates/resources/md-template.gotmpl | 17 ++ .../templates/resources/txt-template.gotmpl | 6 + internal/templates/templates.go | 86 +++++++ 8 files changed, 456 insertions(+), 225 deletions(-) create mode 100644 internal/parser/parser.go rename {cmd/parser/modules => internal/s3}/resources/tests-struct.json (92%) create mode 100644 internal/s3/structure.go create mode 100644 internal/templates/resources/md-template.gotmpl create mode 100644 internal/templates/resources/txt-template.gotmpl create mode 100644 internal/templates/templates.go diff --git a/README.md b/README.md index 06af8ed..9c596ba 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,5 @@ $ ./bin/s3-tests-parser compatibility tests/fixture/suite.csv You can also use json ```shell -$ ./bin/s3-tests-parser compatibility tests/fixture/suite.json --format json +$ ./bin/s3-tests-parser compatibility tests/fixture/suite.json --format json --output-format md --output result.md ``` diff --git a/cmd/parser/modules/compatibility.go b/cmd/parser/modules/compatibility.go index 145fe25..379da61 100644 --- a/cmd/parser/modules/compatibility.go +++ b/cmd/parser/modules/compatibility.go @@ -1,56 +1,65 @@ package modules import ( - "bytes" _ "embed" - "encoding/csv" - "encoding/json" "fmt" "os" + "sort" + "strings" + "git.frostfs.info/TrueCloudLab/s3-tests-parser/internal/parser" + "git.frostfs.info/TrueCloudLab/s3-tests-parser/internal/s3" + "git.frostfs.info/TrueCloudLab/s3-tests-parser/internal/templates" "github.com/spf13/cobra" "github.com/spf13/viper" ) -//go:embed resources/tests-struct.json -var testStructData []byte - var compatibilityCmd = &cobra.Command{ Use: "compatibility", Short: "Shows compatibility results", Long: "Form compatibility table based on passed s3 tests", Example: `s3-tests-parser compatibility suite.csv -s3-tests-parser compatibility suite.json --format json`, +s3-tests-parser compatibility suite.json --format json +s3-tests-parser compatibility suite.json --format json --output-format md +s3-tests-parser compatibility suite.json --format json --output-format md --output result.md`, RunE: runCompatibilityCmd, } type ( - TestsStructure struct { - Groups []Group `json:"groups"` + Results struct { + Legend []Status + TagGroups []TagGroup } - Group struct { - Name string `json:"name"` - Tag string `json:"tag"` - Skip bool `json:"skip"` - SkipReason string `json:"skip_reason"` - Tests []string `json:"tests"` + TagGroup struct { + Name string + Tests []TestResult } -) -var ( - Reset = "\033[0m" - Red = "\033[31m" - Green = "\033[32m" - Yellow = "\033[33m" + Status struct { + Color string + Description string + } + + TestResult struct { + Color string + Name string + Comment string + Passed int + Total int + } ) const ( - formatFlag = "format" + formatFlag = "format" + outputFlag = "output" + outputFormatFlag = "output-format" ) func initCompatibilityCmd() { compatibilityCmd.Flags().String(formatFlag, "csv", "format of input test suite file") + compatibilityCmd.Flags().String(outputFlag, "", "file to write output, if missed the stdout is used") + compatibilityCmd.Flags().String(outputFormatFlag, "txt", "format of output") } func runCompatibilityCmd(cmd *cobra.Command, args []string) error { @@ -58,31 +67,70 @@ func runCompatibilityCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("expected exactly one arg, got: %v", args) } - var testStruct TestsStructure - if err := json.Unmarshal(testStructData, &testStruct); err != nil { - return fmt.Errorf("failed to parse tests struct: %w", err) + testStruct, err := s3.ParseTestsStruct() + if err != nil { + return err } - testsMap, err := parseSuite(args[0], viper.GetString(formatFlag)) + testsMap, err := parser.ParseSuite(args[0], viper.GetString(formatFlag)) if err != nil { return fmt.Errorf("parse tests: %w", err) } - for _, group := range testStruct.Groups { - if group.Skip { - cmd.Println(fmt.Sprintf("%s; skip: %s", group.Name, group.SkipReason)) - continue - } - - printResult(cmd, group, testsMap) - } - - return nil + res := formResults(testStruct, testsMap) + return printResults(cmd, res) } -func printResult(cmd *cobra.Command, group Group, testsMap map[string]bool) { - ln := float64(len(group.Tests)) - pass := 0.0 +var legend = []Status{ + { + Color: templates.GreenColor, + Description: "Supported", + }, + { + Color: templates.YellowColor, + Description: "Partially supported", + }, + { + Color: templates.RedColor, + Description: "Badly supported", + }, + { + Color: templates.BlueColor, + Description: "Not supported yet, but will be in future", + }, + { + Color: templates.BlackColor, + Description: "Not applicable or will never be supported", + }, +} + +func formResults(testStruct s3.TestsStructure, testsMap map[string]bool) Results { + tagGroups := make(map[string]TagGroup) + for _, group := range testStruct.Groups { + tagGroup, ok := tagGroups[group.Tag] + if !ok { + tagGroup.Name = group.Tag + } + + tagGroup.Tests = append(tagGroup.Tests, formTestResult(group, testsMap)) + tagGroups[group.Tag] = tagGroup + } + + res := Results{Legend: legend} + for _, group := range tagGroups { + res.TagGroups = append(res.TagGroups, group) + } + + sort.Slice(res.TagGroups, func(i, j int) bool { + return res.TagGroups[i].Name < res.TagGroups[j].Name + }) + + return res +} + +func formTestResult(group s3.Group, testsMap map[string]bool) TestResult { + ln := len(group.Tests) + pass := 0 for _, test := range group.Tests { if testsMap[test] { @@ -90,95 +138,47 @@ func printResult(cmd *cobra.Command, group Group, testsMap map[string]bool) { } } - color := Red - rate := pass / ln - if rate > 0.9 { - color = Green - } else if rate > 0.5 { - color = Yellow + var color string + if strings.Contains(group.Comment, "Not supported yet") { + color = templates.BlueColor + } else if strings.Contains(group.Comment, "Not applicable") { + color = templates.BlackColor } - cmd.Println(fmt.Sprintf("%s%s: %d/%d \u001B[0m", color, group.Name, int(pass), int(ln))) -} - -func parseSuite(filePath string, format string) (map[string]bool, error) { - switch format { - case "csv": - return parseSuiteCSV(filePath) - case "json": - return parseSuiteJSON(filePath) - default: - return nil, fmt.Errorf("unknown format: %s", format) - } -} - -func parseSuiteCSV(filePath string) (map[string]bool, error) { - data, err := os.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read suite file: %w", err) - } - - rr := csv.NewReader(bytes.NewReader(data)) - records, err := rr.ReadAll() - if err != nil { - return nil, fmt.Errorf("failed to parse suite file: %w", err) - } - - indexName := -1 - indexStatus := -1 - tests := make(map[string]bool) - - for i, recs := range records { - if i == 0 { - for j, rec := range recs { - if rec == "Name" { - indexName = j - } else if rec == "Status" { - indexStatus = j - } - } - - if indexName == -1 || indexStatus == -1 { - return nil, fmt.Errorf("invalid csv format, couldn't find 'Name' and 'Status' fields") - } + if color == "" { + color = templates.RedColor + rate := float64(pass) / float64(ln) + if rate > 0.9 { + color = templates.GreenColor + } else if rate > 0.5 { + color = templates.YellowColor } - - tests[recs[indexName]] = recs[indexStatus] == "passed" } - return tests, nil + return TestResult{ + Color: color, + Name: group.Name, + Comment: group.Comment, + Passed: pass, + Total: ln, + } } -type SuiteNode struct { - Name string `json:"name"` - Status string `json:"status"` - Children []SuiteNode `json:"children"` -} +func printResults(cmd *cobra.Command, res Results) error { + w := cmd.OutOrStdout() + if outFile := viper.GetString(outputFlag); outFile != "" { + f, err := os.Create(outFile) + if err != nil { + return fmt.Errorf("create out file: %w", err) + } + w = f + defer f.Close() + } -func parseSuiteJSON(filePath string) (map[string]bool, error) { - data, err := os.ReadFile(filePath) + outTemplate, err := templates.GetTemplate(viper.GetString(outputFormatFlag)) if err != nil { - return nil, fmt.Errorf("failed to read suite file: %w", err) + return fmt.Errorf("form out template: %w", err) } - var suiteNode SuiteNode - if err = json.Unmarshal(data, &suiteNode); err != nil { - return nil, fmt.Errorf("failed to parse suite file: %w", err) - } - - tests := make(map[string]bool) - parseSuiteNode(suiteNode, tests) - - return tests, nil -} - -func parseSuiteNode(node SuiteNode, res map[string]bool) { - if node.Status != "" { - res[node.Name] = node.Status == "passed" - return - } - - for _, child := range node.Children { - parseSuiteNode(child, res) - } + return outTemplate.Execute(w, res) } diff --git a/internal/parser/parser.go b/internal/parser/parser.go new file mode 100644 index 0000000..6c6c0bc --- /dev/null +++ b/internal/parser/parser.go @@ -0,0 +1,91 @@ +package parser + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "fmt" + "os" +) + +func ParseSuite(filePath string, format string) (map[string]bool, error) { + switch format { + case "csv": + return parseSuiteCSV(filePath) + case "json": + return parseSuiteJSON(filePath) + default: + return nil, fmt.Errorf("unknown format: %s", format) + } +} + +func parseSuiteCSV(filePath string) (map[string]bool, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read suite file: %w", err) + } + + rr := csv.NewReader(bytes.NewReader(data)) + records, err := rr.ReadAll() + if err != nil { + return nil, fmt.Errorf("failed to parse suite file: %w", err) + } + + indexName := -1 + indexStatus := -1 + tests := make(map[string]bool) + + for i, recs := range records { + if i == 0 { + for j, rec := range recs { + if rec == "Name" { + indexName = j + } else if rec == "Status" { + indexStatus = j + } + } + + if indexName == -1 || indexStatus == -1 { + return nil, fmt.Errorf("invalid csv format, couldn't find 'Name' and 'Status' fields") + } + } + + tests[recs[indexName]] = recs[indexStatus] == "passed" + } + + return tests, nil +} + +type suiteNode struct { + Name string `json:"name"` + Status string `json:"status"` + Children []suiteNode `json:"children"` +} + +func parseSuiteJSON(filePath string) (map[string]bool, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read suite file: %w", err) + } + + var suiteNode suiteNode + if err = json.Unmarshal(data, &suiteNode); err != nil { + return nil, fmt.Errorf("failed to parse suite file: %w", err) + } + + tests := make(map[string]bool) + parseSuiteNode(suiteNode, tests) + + return tests, nil +} + +func parseSuiteNode(node suiteNode, res map[string]bool) { + if node.Status != "" { + res[node.Name] = node.Status == "passed" + return + } + + for _, child := range node.Children { + parseSuiteNode(child, res) + } +} diff --git a/cmd/parser/modules/resources/tests-struct.json b/internal/s3/resources/tests-struct.json similarity index 92% rename from cmd/parser/modules/resources/tests-struct.json rename to internal/s3/resources/tests-struct.json index c87acda..c3e1544 100644 --- a/cmd/parser/modules/resources/tests-struct.json +++ b/internal/s3/resources/tests-struct.json @@ -11,7 +11,7 @@ "test_list_multipart_upload_owner" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "CompleteMultipartUpload", @@ -40,7 +40,7 @@ "test_multipart_copy_multiple_sizes" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "CopyObject", @@ -72,7 +72,7 @@ "test_object_copy_key_not_found" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "CreateBucket", @@ -130,7 +130,7 @@ "test_buckets_list_ctime" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "CreateMultipartUpload", @@ -175,7 +175,7 @@ "test_sse_s3_default_multipart_upload" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "DeleteBucket", @@ -190,14 +190,14 @@ "test_atomic_write_bucket_gone" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "DeleteBucketAnalyticsConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketCors", @@ -206,7 +206,7 @@ "test_set_cors" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "DeleteBucketEncryption", @@ -216,42 +216,42 @@ "test_delete_bucket_encryption_kms" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketIntelligentTieringConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketInventoryConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketLifecycle", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketMetricsConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketOwnershipControls", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketPolicy", @@ -261,14 +261,14 @@ "test_bucketv2_policy_acl" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketReplication", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteBucketTagging", @@ -277,14 +277,14 @@ "test_set_bucket_tagging" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "DeleteBucketWebsite", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "DeleteObject", @@ -326,7 +326,7 @@ "test_object_lock_uploading_obj" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "DeleteObjects", @@ -344,7 +344,7 @@ "test_versioning_concurrent_multi_object_delete" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "DeleteObjectTagging", @@ -354,21 +354,21 @@ "test_delete_tags_obj_public" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "DeletePublicAccessBlock", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketAccelerateConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not applicable or will never be supported" + "comment": "Not applicable or will never be supported" }, { "name": "GetBucketAcl", @@ -395,14 +395,14 @@ "test_bucket_acl_grant_userid_writeacp" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetBucketAnalyticsConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketCors", @@ -414,7 +414,7 @@ "test_cors_header_option" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetBucketEncryption", @@ -424,28 +424,28 @@ "test_get_bucket_encryption_kms" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketIntelligentTieringConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketInventoryConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketLifecycle", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketLifecycleConfiguration", @@ -455,7 +455,7 @@ "test_lifecycle_get_no_id" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketLocation", @@ -464,7 +464,7 @@ "test_bucket_get_location" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetBucketLogging", @@ -473,35 +473,35 @@ "test_logging_toggle" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketMetricsConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketNotification", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketNotificationConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketOwnershipControls", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketPolicy", @@ -512,7 +512,7 @@ "test_bucket_policy_set_condition_operator_end_with_IfExists" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetBucketPolicyStatus", @@ -526,21 +526,21 @@ "test_get_nonpublicpolicy_deny_bucket_policy_status" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketReplication", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetBucketRequestPayment", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not applicable or will never be supported" + "comment": "Not applicable or will never be supported" }, { "name": "GetBucketTagging", @@ -549,7 +549,7 @@ "test_set_bucket_tagging" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetBucketVersioning", @@ -563,14 +563,14 @@ "test_versioning_obj_plain_null_version_overwrite_suspended" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetBucketWebsite", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "GetObject", @@ -634,7 +634,7 @@ "test_bucket_create_special_key_names" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetObjectAcl", @@ -664,14 +664,14 @@ "test_bucket_policy_get_obj_acl_existing_tag" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetObjectAttributes", "tag": "API", "tests": [], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetObjectLegalHold", @@ -681,7 +681,7 @@ "test_object_lock_get_legal_hold_invalid_bucket" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetObjectLockConfiguration", @@ -691,7 +691,7 @@ "test_object_lock_get_obj_lock_invalid_bucket" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetObjectRetention", @@ -705,7 +705,7 @@ "test_object_lock_put_obj_retention_shorten_period_bypass" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetObjectTagging", @@ -728,7 +728,7 @@ "test_bucket_policy_get_obj_acl_existing_tag" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "GetObjectTorrent", @@ -737,7 +737,7 @@ "test_get_object_torrent" ], "skip": true, - "skip_reason": "Not applicable or will never be supported" + "comment": "Not applicable or will never be supported" }, { "name": "GetPublicAccessBlock", @@ -746,7 +746,7 @@ "test_get_default_public_block" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "HeadBucket", @@ -758,7 +758,7 @@ "test_bucket_head_extended" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "HeadObject", @@ -782,35 +782,35 @@ "test_sse_s3_default_method_head" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "ListBucketAnalyticsConfigurations", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "ListBucketIntelligentTieringConfigurations", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "ListBucketInventoryConfigurations", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "ListBucketMetricsConfigurations", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "ListBuckets", @@ -824,7 +824,7 @@ "test_list_buckets_bad_auth" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "ListMultipartUploads", @@ -835,7 +835,7 @@ "test_lifecycle_multipart_expiration" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "ListObjects", @@ -896,7 +896,7 @@ "test_bucket_list_delimiter_prefix_underscore" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "ListObjectsV2", @@ -963,7 +963,7 @@ "test_sse_s3_default_multipart_upload" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "ListObjectVersions", @@ -992,21 +992,21 @@ "test_versioning_bucket_multipart_upload_return_version_id" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "ListParts", "tag": "API", "tests": [], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutBucketAccelerateConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not applicable or will never be supported" + "comment": "Not applicable or will never be supported" }, { "name": "PutBucketAcl", @@ -1052,14 +1052,14 @@ "test_access_bucket_publicreadwrite_object_publicreadwrite" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutBucketAnalyticsConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketCors", @@ -1073,7 +1073,7 @@ "test_cors_presigned_get_object_tenant" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutBucketEncryption", @@ -1095,28 +1095,28 @@ "test_sse_kms_default_upload_8mb" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketIntelligentTieringConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketInventoryConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketLifecycle", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketLifecycleConfiguration", @@ -1160,7 +1160,7 @@ "test_lifecycle_cloud_transition_large_obj" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketLogging", @@ -1169,35 +1169,35 @@ "test_logging_toggle" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketMetricsConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketNotification", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketNotificationConfiguration", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketOwnershipControls", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketPolicy", @@ -1226,21 +1226,21 @@ "test_multipart_upload_on_a_bucket_with_policy" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutBucketReplication", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutBucketRequestPayment", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not applicable or will never be supported" + "comment": "Not applicable or will never be supported" }, { "name": "PutBucketTagging", @@ -1249,7 +1249,7 @@ "test_set_bucket_tagging" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutBucketVersioning", @@ -1281,14 +1281,14 @@ "test_versioning_bucket_multipart_upload_return_version_id" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutBucketWebsite", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "PutObject", @@ -1340,7 +1340,7 @@ "test_object_acl_canned" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutObjectAcl", @@ -1372,7 +1372,7 @@ "test_access_bucket_publicreadwrite_object_publicreadwrite" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutObjectLegalHold", @@ -1388,7 +1388,7 @@ "test_object_lock_uploading_obj" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutObjectLockConfiguration", @@ -1405,7 +1405,7 @@ "test_object_lock_put_obj_retention_override_default_retention" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutObjectRetention", @@ -1430,7 +1430,7 @@ "test_object_lock_changing_mode_from_compliance" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutObjectTagging", @@ -1460,7 +1460,7 @@ "test_bucket_policy_get_obj_acl_existing_tag" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "PutPublicAccessBlock", @@ -1473,14 +1473,14 @@ "test_ignore_public_acls" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "RestoreObject", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "SelectObjectContent", @@ -1522,7 +1522,7 @@ "test_output_serial_expressions" ], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "UploadPart", @@ -1550,7 +1550,7 @@ "test_list_multipart_upload_owner" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "UploadPartCopy", @@ -1565,18 +1565,18 @@ "test_multipart_copy_multiple_sizes" ], "skip": false, - "skip_reason": "" + "comment": "" }, { "name": "WriteGetObjectResponse", "tag": "API", "tests": [], "skip": true, - "skip_reason": "Not supported yet, but will be in future" + "comment": "Not supported yet, but will be in future" }, { "name": "encryption", - "tag": "feature", + "tag": "Features", "tests": [ "test_sse_kms_method_head", "test_sse_kms_present", @@ -1626,11 +1626,11 @@ "test_encryption_sse_c_post_object_authenticated_request" ], "skip": false, - "skip_reason": "" + "comment": "Only sse-c currently supported" }, { "name": "post_upload", - "tag": "feature", + "tag": "Features", "tests": [ "test_post_object_anonymous_request", "test_post_object_authenticated_request", @@ -1671,11 +1671,11 @@ "test_sse_kms_post_object_authenticated_request" ], "skip": false, - "skip_reason": "" + "comment": "Supported only basic POST upload" }, { "name": "aws_v2_signature", - "tag": "feature", + "tag": "Features", "tests": [ "test_object_create_bad_md5_invalid_garbage_aws2", "test_object_create_bad_contentlength_mismatch_below_aws2", @@ -1691,11 +1691,11 @@ "test_object_create_bad_date_after_end_aws2" ], "skip": false, - "skip_reason": "" + "comment": "AWS v2 signature is deprecated" }, { "name": "locking", - "tag": "feature", + "tag": "Features", "tests": [ "test_object_lock_put_obj_lock", "test_object_lock_put_obj_lock_invalid_bucket", @@ -1728,7 +1728,7 @@ "test_object_lock_get_obj_metadata" ], "skip": false, - "skip_reason": "" + "comment": "" } ] } \ No newline at end of file diff --git a/internal/s3/structure.go b/internal/s3/structure.go new file mode 100644 index 0000000..f333156 --- /dev/null +++ b/internal/s3/structure.go @@ -0,0 +1,31 @@ +package s3 + +import ( + _ "embed" + "encoding/json" + "fmt" +) + +//go:embed resources/tests-struct.json +var testStructData []byte + +type TestsStructure struct { + Groups []Group `json:"groups"` +} + +type Group struct { + Name string `json:"name"` + Tag string `json:"tag"` + Skip bool `json:"skip"` + Comment string `json:"comment"` + Tests []string `json:"tests"` +} + +func ParseTestsStruct() (TestsStructure, error) { + var testStruct TestsStructure + if err := json.Unmarshal(testStructData, &testStruct); err != nil { + return TestsStructure{}, fmt.Errorf("failed to parse tests struct: %w", err) + } + + return testStruct, nil +} diff --git a/internal/templates/resources/md-template.gotmpl b/internal/templates/resources/md-template.gotmpl new file mode 100644 index 0000000..4955e6a --- /dev/null +++ b/internal/templates/resources/md-template.gotmpl @@ -0,0 +1,17 @@ +# S3 Protocol Compatibility + +Reference: +* [AWS S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/s3-api.pdf) + +| | Legend | +|---|--------|{{range .Legend}} +| {{colorToCircle .Color}} | {{.Description}} |{{end}} + + +{{range .TagGroups}} +## {{.Name}} + +| | Name | Comments | +|---|------|----------|{{range .Tests}} +| {{colorToCircle .Color}} | {{.Name}} | {{.Comment}} |{{end}} +{{end}} diff --git a/internal/templates/resources/txt-template.gotmpl b/internal/templates/resources/txt-template.gotmpl new file mode 100644 index 0000000..63d6cfe --- /dev/null +++ b/internal/templates/resources/txt-template.gotmpl @@ -0,0 +1,6 @@ +# S3 Protocol Compatibility +{{range .TagGroups}} +## {{.Name}} +{{range .Tests}} +{{colorToTerminal .Color}}{{.Name}}: {{.Passed}}/{{.Total}}; {{.Comment}} {{colorToTerminal "black"}} {{end}} +{{end}} diff --git a/internal/templates/templates.go b/internal/templates/templates.go new file mode 100644 index 0000000..fc79875 --- /dev/null +++ b/internal/templates/templates.go @@ -0,0 +1,86 @@ +package templates + +import ( + _ "embed" + "fmt" + "text/template" +) + +const ( + GreenColor = "green" + YellowColor = "yellow" + RedColor = "red" + BlueColor = "blue" + BlackColor = "black" +) + +const ( + blackTerminal = "\033[0m" + redTerminal = "\033[31m" + greenTerminal = "\033[32m" + yellowTerminal = "\033[33m" + blueTerminal = "\033[34m" +) + +const ( + greenCircle = "🟢" + yellowCircle = "🟡" + redCircle = "🔴" + blueCircle = "🔵" + blackCircle = "⚫" +) + +var ( + //go:embed resources/txt-template.gotmpl + txtTemplate []byte + //go:embed resources/md-template.gotmpl + mdTemplate []byte +) + +func GetTemplate(outFormat string) (*template.Template, error) { + var templateData []byte + + switch outFormat { + case "txt": + templateData = txtTemplate + case "md": + templateData = mdTemplate + default: + return nil, fmt.Errorf("unkonwn output format: %s", outFormat) + } + + return template.New("out-format").Funcs(template.FuncMap{ + "colorToCircle": ColorToCircle, + "colorToTerminal": ColorToTerminal, + }).Parse(string(templateData)) +} + +func ColorToCircle(color string) string { + switch color { + case GreenColor: + return greenCircle + case YellowColor: + return yellowCircle + case RedColor: + return redCircle + case BlueColor: + return blueCircle + default: + return blackCircle + } +} + +func ColorToTerminal(color string) string { + switch color { + case GreenColor: + return greenTerminal + case YellowColor: + return yellowTerminal + case RedColor: + return redTerminal + case BlueColor: + return blueTerminal + default: + return blackTerminal + } +}