Support result formatting

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
Denis Kirillov 2023-09-01 12:08:49 +03:00
parent 8ae5fa912b
commit ca943e1dc8
8 changed files with 456 additions and 225 deletions

View file

@ -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
```

View file

@ -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)
}

91
internal/parser/parser.go Normal file
View file

@ -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)
}
}

View file

@ -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": ""
}
]
}

31
internal/s3/structure.go Normal file
View file

@ -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
}

View file

@ -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}}

View file

@ -0,0 +1,6 @@
# S3 Protocol Compatibility
{{range .TagGroups}}
## {{.Name}}
{{range .Tests}}
{{colorToTerminal .Color}}{{.Name}}: {{.Passed}}/{{.Total}}; {{.Comment}} {{colorToTerminal "black"}} {{end}}
{{end}}

View file

@ -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
}
}