diff --git a/.gitignore b/.gitignore index e87a764..1a1e5b5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,5 @@ armstrong armstrong.exe # test output -coverage/test_coverage_report*.md +coverage/testdata/test_coverage_report*.md coverage/testdata/index.json diff --git a/commands/credential_scan.go b/commands/credential_scan.go index 8dee80d..de983a9 100644 --- a/commands/credential_scan.go +++ b/commands/credential_scan.go @@ -235,7 +235,7 @@ func (c CredentialScanCommand) Execute() int { var swaggerModel *coverage.SwaggerModel if c.swaggerRepoPath != "" { logrus.Infof("scan based on local swagger repo: %s", c.swaggerRepoPath) - swaggerModel, err = coverage.GetModelInfoFromLocalIndex(mockedResourceId, apiVersion, c.swaggerRepoPath, c.swaggerIndexFile) + swaggerModel, err = coverage.GetModelInfoFromLocalIndex(mockedResourceId, apiVersion, "PUT", c.swaggerRepoPath, c.swaggerIndexFile) if err != nil { credScanErr := makeCredScanError( azapiResource, @@ -248,7 +248,7 @@ func (c CredentialScanCommand) Execute() int { continue } } else { - swaggerModel, err = coverage.GetModelInfoFromIndex(mockedResourceId, apiVersion, c.swaggerIndexFile) + swaggerModel, err = coverage.GetModelInfoFromIndex(mockedResourceId, apiVersion, "PUT", c.swaggerIndexFile) if err != nil { credScanErr := makeCredScanError( azapiResource, diff --git a/coverage/coverage.go b/coverage/coverage.go index fb69081..d57918f 100644 --- a/coverage/coverage.go +++ b/coverage/coverage.go @@ -22,14 +22,17 @@ type Model struct { IsFullyCovered bool `json:"IsFullyCovered,omitempty"` IsReadOnly bool `json:"IsReadOnly,omitempty"` IsRequired bool `json:"IsRequired,omitempty"` + IsRoot bool `json:"IsRoot,omitempty"` IsSecret bool `json:"IsSecret,omitempty"` // related to x-ms-secret Item *Model `json:"Item,omitempty"` ModelName string `json:"ModelName,omitempty"` Properties *map[string]*Model `json:"Properties,omitempty"` + RootCoveredCount int `json:"RootCoveredCount,omitempty"` // only for root model, covered count plus all variant count if any + RootTotalCount int `json:"RootTotalCount,omitempty"` // only for root model, total count plus all variant count if any SourceFile string `json:"SourceFile,omitempty"` TotalCount int `json:"TotalCount,omitempty"` Type *string `json:"Type,omitempty"` - Variants *map[string]*Model `json:"Variants,omitempty"` // variant model name is used as key, this may only contains + Variants *map[string]*Model `json:"Variants,omitempty"` // variant model name is used as key, in case x-ms-discriminator-value is not available VariantType *string `json:"VariantType,omitempty"` // the x-ms-discriminator-value of the variant model if exists, otherwise model name } @@ -90,13 +93,14 @@ func (m *Model) CredScan(root interface{}, secrets map[string]string) { if isMatchProperty { for k, v := range value { if m.Properties == nil { - if !m.HasAdditionalProperties { - logrus.Errorf("unexpected key %s in %s", k, m.Identifier) - } + // some objects has no properties defined + // https://github.com/Azure/azure-rest-api-specs/blob/3519c80fe510a268f6e59a29ccac8a53fdec15b6/specification/monitor/resource-manager/Microsoft.Insights/stable/2023-03-11/dataCollectionRules_API.json#L724 + + logrus.Warnf("unexpected key %s in %s", k, m.Identifier) continue } if _, ok := (*m.Properties)[k]; !ok { - if !m.HasAdditionalProperties { + if !m.HasAdditionalProperties && m.Discriminator == nil { logrus.Errorf("unexpected key %s in %s", k, m.Identifier) continue } @@ -128,7 +132,7 @@ func (m *Model) MarkCovered(root interface{}) { if m.Enum != nil { strValue := fmt.Sprintf("%v", value) if _, ok := (*m.Enum)[strValue]; !ok { - logrus.Errorf("unexpected enum %s in %s", value, m.Identifier) + logrus.Warningf("unexpected enum %s in %s", value, m.Identifier) } (*m.Enum)[strValue] = true @@ -168,14 +172,14 @@ func (m *Model) MarkCovered(root interface{}) { // either the discriminator value hit the variant model name or variant type, we match the variant if variant, ok := (*m.Variants)[v.(string)]; ok { - isMatchProperty = false + isMatchProperty = true variant.MarkCovered(value) break } for _, variant := range *m.Variants { if variant.VariantType != nil && *variant.VariantType == v.(string) { - isMatchProperty = false + isMatchProperty = true variant.MarkCovered(value) break Loop @@ -189,13 +193,14 @@ func (m *Model) MarkCovered(root interface{}) { if isMatchProperty { for k, v := range value { if m.Properties == nil { - if !m.HasAdditionalProperties { - logrus.Errorf("unexpected key %s in %s", k, m.Identifier) - } + // some objects has no properties defined + // https://github.com/Azure/azure-rest-api-specs/blob/3519c80fe510a268f6e59a29ccac8a53fdec15b6/specification/monitor/resource-manager/Microsoft.Insights/stable/2023-03-11/dataCollectionRules_API.json#L724 + logrus.Warnf("unexpected key %s in %s", k, m.Identifier) + continue } if _, ok := (*m.Properties)[k]; !ok { - if !m.HasAdditionalProperties { + if !m.HasAdditionalProperties && m.Discriminator == nil { logrus.Errorf("unexpected key %s in %s", k, m.Identifier) continue } @@ -272,6 +277,20 @@ func (m *Model) CountCoverage() (int, int) { } } + if m.IsRoot { + if m.Variants != nil { + for _, v := range *m.Variants { + v.CountCoverage() + } + } + + if m.Item != nil && m.Item.Variants != nil { + for _, v := range *m.Item.Variants { + v.CountCoverage() + } + } + } + if m.TotalCount == 0 { m.TotalCount = 1 } @@ -281,6 +300,23 @@ func (m *Model) CountCoverage() (int, int) { m.IsFullyCovered = m.TotalCount > 0 && m.CoveredCount == m.TotalCount + if m.IsRoot { + m.RootCoveredCount = m.CoveredCount + m.RootTotalCount = m.TotalCount + if m.Variants != nil { + for _, v := range *m.Variants { + m.RootCoveredCount += v.CoveredCount + m.RootTotalCount += v.TotalCount + } + } + if m.Item != nil && m.Item.Variants != nil { + for _, v := range *m.Item.Variants { + m.RootCoveredCount += v.CoveredCount + m.RootTotalCount += v.TotalCount + } + } + } + return m.CoveredCount, m.TotalCount } diff --git a/coverage/coverage_test.go b/coverage/coverage_test.go index 03c1219..748626c 100644 --- a/coverage/coverage_test.go +++ b/coverage/coverage_test.go @@ -15,11 +15,13 @@ import ( ) type testCase struct { - name string - apiVersion string - apiPath string - rawRequest []string - resourceType string + name string + apiPath string + rawRequest []string + resourceType string + method string + expectedCoveredCount int + expectedTotalCount int } func normarlizePath(path string) string { @@ -28,10 +30,12 @@ func normarlizePath(path string) string { func TestCoverage_ResourceGroup(t *testing.T) { tc := testCase{ - name: "ResourceGroup", - resourceType: "Microsoft.Resources/resourceGroups@2022-09-01", - apiVersion: "2022-09-01", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rgName", + name: "ResourceGroup", + resourceType: "Microsoft.Resources/resourceGroups@2022-09-01", + method: "PUT", + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rgName", + expectedCoveredCount: 1, + expectedTotalCount: 3, rawRequest: []string{ `{ "location": "westeurope" @@ -44,14 +48,6 @@ func TestCoverage_ResourceGroup(t *testing.T) { t.Fatalf("process coverage: %+v", err) } - if model.CoveredCount != 1 { - t.Fatalf("expected CoveredCount 1, got %d", model.CoveredCount) - } - - if model.TotalCount != 3 { - t.Fatalf("expected TotalCount 3, got %d", model.TotalCount) - } - if model.Properties == nil { t.Fatalf("expected properties, got none") } @@ -67,11 +63,12 @@ func TestCoverage_ResourceGroup(t *testing.T) { func TestCoverage_HealthcareDicom(t *testing.T) { tc := testCase{ - name: "HealthcareApisDicom", - resourceType: "Microsoft.HealthcareApis/workspaces/dicomservices@2024-03-31", - apiVersion: "2024-03-31", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rgName/providers/Microsoft.HealthcareApis/workspaces/workspaceName/dicomservices/dicomServiceName", - // bulkImportConfiguration property not in swagger + name: "HealthcareApisDicom", + resourceType: "Microsoft.HealthcareApis/workspaces/dicomservices@2024-03-31", + method: "PUT", + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rgName/providers/Microsoft.HealthcareApis/workspaces/workspaceName/dicomservices/dicomServiceName", + expectedCoveredCount: 12, + expectedTotalCount: 14, rawRequest: []string{ `{ "identity": { @@ -100,9 +97,6 @@ func TestCoverage_HealthcareDicom(t *testing.T) { "*" ] }, - "bulkImportConfiguration": { - "enabled": false - }, "enableDataPartitions": false, "encryption": { "customerManagedKeyEncryption": { @@ -118,22 +112,20 @@ func TestCoverage_HealthcareDicom(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - if model.CoveredCount != 12 { - t.Fatalf("expected CoveredCount 12, got %d", model.CoveredCount) - } } func TestCoverage_MachineLearningServicesWorkspacesJobs(t *testing.T) { tc := testCase{ - name: "MachineLearningServicesWorkspacesJobs", - resourceType: "Microsoft.MachineLearningServices/workspaces/jobs", - apiVersion: "2023-06-01-preview", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.MachineLearningServices/workspaces/works1/jobs/job1", + name: "MachineLearningServicesWorkspacesJobs", + resourceType: "Microsoft.MachineLearningServices/workspaces/jobs@2024-04-01", + method: "PUT", + expectedCoveredCount: 19, + expectedTotalCount: 895, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.MachineLearningServices/workspaces/works1/jobs/job1", rawRequest: []string{ `{ "properties": { @@ -179,23 +171,20 @@ func TestCoverage_MachineLearningServicesWorkspacesJobs(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 11 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_MachineLearningServicesWorkspacesDataVersions(t *testing.T) { tc := testCase{ - name: "MachineLearningServicesWorkspacesDataVersions", - resourceType: "Microsoft.MachineLearningServices/workspaces/data/versions", - apiVersion: "2023-06-01-preview", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.MachineLearningServices/workspaces/works1/data/data1/versions/version1", + name: "MachineLearningServicesWorkspacesDataVersions", + resourceType: "Microsoft.MachineLearningServices/workspaces/data/versions@2024-04-01", + method: "PUT", + expectedCoveredCount: 15, + expectedTotalCount: 29, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.MachineLearningServices/workspaces/works1/data/data1/versions/version1", rawRequest: []string{ `{ "properties": { @@ -218,23 +207,20 @@ func TestCoverage_MachineLearningServicesWorkspacesDataVersions(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 8 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_DeviceSecurityGroup(t *testing.T) { tc := testCase{ - name: "DeviceSecurityGroup", - resourceType: "Microsoft.Security/deviceSecurityGroups@2019-08-01", - apiVersion: "2019-08-01", - apiPath: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/SampleRG/providers/Microsoft.Devices/iotHubs/sampleiothub/providers/Microsoft.Security/deviceSecurityGroups/samplesecuritygroup", + name: "DeviceSecurityGroup", + resourceType: "Microsoft.Security/deviceSecurityGroups@2019-08-01", + method: "PUT", + expectedCoveredCount: 10, + expectedTotalCount: 192, + apiPath: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/SampleRG/providers/Microsoft.Devices/iotHubs/sampleiothub/providers/Microsoft.Security/deviceSecurityGroups/samplesecuritygroup", rawRequest: []string{ `{ "properties": { @@ -253,30 +239,31 @@ func TestCoverage_DeviceSecurityGroup(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 5 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } -func TestCoverage_DataMigrationServiceTasks(t *testing.T) { - // Do we need to support cross file discriminator reference? Now seems only DataMigration has this. e.g., https://github.com/Azure/azure-rest-api-specs/blob/0ab5469dc0d75594f5747493dcfe8774e22d728f/specification/datamigration/resource-manager/Microsoft.DataMigration/stable/2021-06-30/definitions/ServiceTasks.json#L39 +func TestCoverage_SCVMM(t *testing.T) { tc := testCase{ - name: "DataMigrationServiceTasks", - resourceType: "Microsoft.DataMigration/services/serviceTasks@2021-06-30", - apiVersion: "2021-06-30", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/DmsSdkRg/providers/Microsoft.DataMigration/services/DmsSdkService/serviceTasks/DmsSdkTask", + name: "SCVMM", + resourceType: "Microsoft.ScVmm/virtualMachineInstances@2023-10-07", + method: "PUT", + expectedCoveredCount: 5, + expectedTotalCount: 39, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/cli-test-rg-vmm/providers/Microsoft.HybridCompute/machines/test-vm-tf/providers/Microsoft.ScVmm/virtualMachineInstances/default", rawRequest: []string{ `{ + "extendedLocation": { + "name": "/subscriptions/12345678-1234-9876-4563-123456789012/resourcegroups/syntheticsscvmmcan/providers/microsoft.extendedlocation/customlocations/terraform-test-cl", + "type": "customLocation" + }, "properties": { - "taskType": "Service.Check.OCI", - "input": { - "serverVersion": "NA" + "infrastructureProfile": { + "cloudId": "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/cli-test-rg-vmm/providers/Microsoft.SCVMM/Clouds/azcli-test-cloud", + "templateId": "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/cli-test-rg-vmm/providers/Microsoft.SCVMM/VirtualMachineTemplates/azcli-test-vm-template-win19", + "vmmServerId": "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/SyntheticsSCVMMCan/providers/microsoft.scvmm/vmmservers/tf-test-vmmserver" } } }`, @@ -289,46 +276,43 @@ func TestCoverage_DataMigrationServiceTasks(t *testing.T) { } } -func TestCoverage_SCVMM(t *testing.T) { +func TestCoverage_DataMigrationServiceTasks(t *testing.T) { + // Do we need to support cross file discriminator reference? Now seems only DataMigration has this. e.g., https://github.com/Azure/azure-rest-api-specs/blob/0ab5469dc0d75594f5747493dcfe8774e22d728f/specification/datamigration/resource-manager/Microsoft.DataMigration/stable/2021-06-30/definitions/ServiceTasks.json#L39 tc := testCase{ - name: "SCVMM", - resourceType: "Microsoft.ScVmm/virtualMachineInstances@2023-10-07", - apiVersion: "2023-10-07", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/cli-test-rg-vmm/providers/Microsoft.HybridCompute/machines/test-vm-tf/providers/Microsoft.ScVmm/virtualMachineInstances/default", + name: "DataMigrationServiceTasks", + resourceType: "Microsoft.DataMigration/services/serviceTasks@2021-06-30", + method: "PUT", + expectedCoveredCount: 3, + expectedTotalCount: 615, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/DmsSdkRg/providers/Microsoft.DataMigration/services/DmsSdkService/serviceTasks/DmsSdkTask", rawRequest: []string{ `{ - "extendedLocation": { - "name": "/subscriptions/12345678-1234-9876-4563-123456789012/resourcegroups/syntheticsscvmmcan/providers/microsoft.extendedlocation/customlocations/terraform-test-cl", - "type": "customLocation" - }, "properties": { - "infrastructureProfile": { - "cloudId": "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/cli-test-rg-vmm/providers/Microsoft.SCVMM/Clouds/azcli-test-cloud", - "templateId": "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/cli-test-rg-vmm/providers/Microsoft.SCVMM/VirtualMachineTemplates/azcli-test-vm-template-win19", - "vmmServerId": "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/SyntheticsSCVMMCan/providers/microsoft.scvmm/vmmservers/tf-test-vmmserver" + "taskType": "ConnectToSource.MySql", + "input": { + "sourceConnectionInfo": { + "serverName": "mySqlService" + } } } }`, }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 5 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_DataMigrationTasks(t *testing.T) { tc := testCase{ - name: "DataMigrationTasks", - resourceType: "Microsoft.DataMigration/services/projects/tasks@2021-06-30", - apiVersion: "2021-06-30", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/DmsSdkRg/providers/Microsoft.DataMigration/services/DmsSdkService/projects/DmsSdkProject/tasks/DmsSdkTask", + name: "DataMigrationTasks", + resourceType: "Microsoft.DataMigration/services/projects/tasks@2021-06-30", + method: "PUT", + expectedCoveredCount: 9, + expectedTotalCount: 615, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/DmsSdkRg/providers/Microsoft.DataMigration/services/DmsSdkService/projects/DmsSdkProject/tasks/DmsSdkTask", rawRequest: []string{ `{ "properties": { @@ -349,23 +333,20 @@ func TestCoverage_DataMigrationTasks(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 8 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_KeyVault(t *testing.T) { tc := testCase{ - name: "KeyVault", - resourceType: "Microsoft.KeyVault/vaults@2023-02-01", - apiVersion: "2023-02-01", - apiPath: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/sample-resource-group/providers/Microsoft.KeyVault/vaults/sample-vault", + name: "KeyVault", + resourceType: "Microsoft.KeyVault/vaults@2023-02-01", + method: "PUT", + expectedCoveredCount: 13, + expectedTotalCount: 28, + apiPath: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/sample-resource-group/providers/Microsoft.KeyVault/vaults/sample-vault", rawRequest: []string{ `{ "location": "westus", @@ -436,23 +417,20 @@ func TestCoverage_KeyVault(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 13 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_StorageAccount(t *testing.T) { tc := testCase{ - name: "StorageAccount", - resourceType: "Microsoft.Storage/storageAccounts@2022-09-01", - apiVersion: "2022-09-01", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/res9101/providers/Microsoft.Storage/storageAccounts/sto4445", + name: "StorageAccount", + resourceType: "Microsoft.Storage/storageAccounts@2023-01-01", + method: "PUT", + expectedCoveredCount: 24, + expectedTotalCount: 69, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/res9101/providers/Microsoft.Storage/storageAccounts/sto4445", rawRequest: []string{ `{ "sku": { @@ -505,23 +483,20 @@ func TestCoverage_StorageAccount(t *testing.T) { }`, }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 24 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_VM(t *testing.T) { tc := testCase{ - name: "VM", - resourceType: "Microsoft.Compute/virtualMachines@2023-03-01", - apiVersion: "2023-03-01", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm", + name: "VM", + resourceType: "Microsoft.Compute/virtualMachines@2024-03-01", + method: "PUT", + expectedCoveredCount: 20, + expectedTotalCount: 166, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm", rawRequest: []string{ `{ "location": "westus", @@ -576,23 +551,20 @@ func TestCoverage_VM(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 20 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_VNet(t *testing.T) { tc := testCase{ - name: "VNet", - resourceType: "Microsoft.Network/virtualNetworks@2023-02-01", - apiVersion: "2023-02-01", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/virtualNetwork", + name: "VNet", + resourceType: "Microsoft.Network/virtualNetworks@2024-01-01", + method: "PUT", + expectedCoveredCount: 4, + expectedTotalCount: 103, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/virtualNetwork", rawRequest: []string{ `{ "properties": { @@ -615,23 +587,20 @@ func TestCoverage_VNet(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 4 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_DataCollectionRule(t *testing.T) { tc := testCase{ - name: "DataCollectionRule", - resourceType: "Microsoft.Insights/dataCollectionRules@2022-06-01", - apiVersion: "2022-06-01", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/test-resources/providers/Microsoft.Insights/dataCollectionRules/testDCR", + name: "DataCollectionRule", + resourceType: "Microsoft.Insights/dataCollectionRules@2022-06-01", + method: "PUT", + expectedCoveredCount: 65, + expectedTotalCount: 65, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/test-resources/providers/Microsoft.Insights/dataCollectionRules/testDCR", rawRequest: []string{ `{ "location": "westeurope", @@ -905,8 +874,7 @@ func TestCoverage_DataCollectionRule(t *testing.T) { "extensions": [ { "streams": [ - "Microsoft-WindowsEvent", - "Microsoft-ServiceMap" + "Microsoft-WindowsEvent" ], "inputDataSources": [ "test-datasource-wineventlog" @@ -955,10 +923,12 @@ func TestCoverage_DataCollectionRule(t *testing.T) { func TestCoverage_WebSite(t *testing.T) { tc := testCase{ - name: "WebSites", - resourceType: "Microsoft.Web/sites@2022-09-01", - apiVersion: "2022-09-01", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/testrg123/providers/Microsoft.Web/sites/sitef6141", + name: "WebSites", + resourceType: "Microsoft.Web/sites@2023-01-01", + method: "PUT", + expectedCoveredCount: 3, + expectedTotalCount: 198, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/testrg123/providers/Microsoft.Web/sites/sitef6141", rawRequest: []string{ `{ "kind": "app", @@ -970,23 +940,20 @@ func TestCoverage_WebSite(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 3 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_AKS(t *testing.T) { tc := testCase{ - name: "AKS", - resourceType: "Microsoft.ContainerService/ManagedClusters@2023-05-02-preview", - apiVersion: "2023-05-02-preview", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourcegroups/rg1/providers/Microsoft.ContainerService/managedClusters/clustername1", + name: "AKS", + resourceType: "Microsoft.ContainerService/ManagedClusters@2024-05-01", + method: "PUT", + expectedCoveredCount: 33, + expectedTotalCount: 234, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourcegroups/rg1/providers/Microsoft.ContainerService/managedClusters/clustername1", rawRequest: []string{`{ "location": "location1", "tags": { @@ -994,7 +961,7 @@ func TestCoverage_AKS(t *testing.T) { "archv2": "" }, "sku": { - "name": "Basic", + "name": "Base", "tier": "Free" }, "properties": { @@ -1060,23 +1027,20 @@ func TestCoverage_AKS(t *testing.T) { }`}, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 33 - if model.CoveredCount != expected { - t.Fatalf("expected TotalCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_CosmosDB(t *testing.T) { tc := testCase{ - name: "CosmosDB", - resourceType: "Microsoft.DocumentDB/databaseAccounts@2023-04-15", - apiVersion: "2023-04-15", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/testdb", + name: "CosmosDB", + resourceType: "Microsoft.DocumentDB/databaseAccounts@2024-05-15", + method: "PUT", + expectedCoveredCount: 34, + expectedTotalCount: 67, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/testdb", rawRequest: []string{ `{ "location": "westus", @@ -1165,11 +1129,6 @@ func TestCoverage_CosmosDB(t *testing.T) { t.Fatalf("process coverage: %v", err) } - expected := 33 - if model.CoveredCount != expected { - t.Fatalf("expected CoveredCount %d, got %d", expected, model.CoveredCount) - } - if model.Properties == nil { t.Fatalf("expected properties, got none") } @@ -1301,10 +1260,12 @@ func TestCoverage_CosmosDB(t *testing.T) { func TestCoverage_DataFactoryPipelines(t *testing.T) { tc := testCase{ - name: "DataFactoryPipelines", - apiVersion: "2018-06-01", - resourceType: "Microsoft.DataFactory/factories/pipelines@2018-06-01", - apiPath: "/subscriptions/12345678-1234-1234-1234-12345678abc/resourceGroups/exampleResourceGroup/providers/Microsoft.DataFactory/factories/exampleFactoryName/pipelines/examplePipeline", + name: "DataFactoryPipelines", + resourceType: "Microsoft.DataFactory/factories/pipelines@2018-06-01", + method: "PUT", + expectedCoveredCount: 13, + expectedTotalCount: 7239, + apiPath: "/subscriptions/12345678-1234-1234-1234-12345678abc/resourceGroups/exampleResourceGroup/providers/Microsoft.DataFactory/factories/exampleFactoryName/pipelines/examplePipeline", rawRequest: []string{ `{ "properties": { @@ -1389,23 +1350,20 @@ func TestCoverage_DataFactoryPipelines(t *testing.T) { }, } - model, err := testCoverage(t, tc) + _, err := testCoverage(t, tc) if err != nil { t.Fatalf("process coverage: %+v", err) } - - expected := 11 - if model.CoveredCount != expected { - t.Fatalf("expected TotalCount %d, got %d", expected, model.CoveredCount) - } } func TestCoverage_DataFactoryLinkedServices(t *testing.T) { tc := testCase{ - name: "DataFactoryLinkedServices", - resourceType: "Microsoft.DataFactory/factories/linkedServices@2018-06-01", - apiVersion: "2018-06-01", - apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.DataFactory/factories/factory1/linkedServices/linked", + name: "DataFactoryLinkedServices", + resourceType: "Microsoft.DataFactory/factories/linkedServices@2018-06-01", + method: "PUT", + expectedCoveredCount: 3, + expectedTotalCount: 3450, + apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.DataFactory/factories/factory1/linkedServices/linked", rawRequest: []string{ `{ "properties": { @@ -1426,11 +1384,6 @@ func TestCoverage_DataFactoryLinkedServices(t *testing.T) { t.Fatalf("process coverage: %+v", err) } - expected := 2 - if model.CoveredCount != expected { - t.Fatalf("expected TotalCount %d, got %d", expected, model.CoveredCount) - } - if model.Properties == nil { t.Fatalf("expected properties, got none") } @@ -1443,8 +1396,8 @@ func TestCoverage_DataFactoryLinkedServices(t *testing.T) { t.Fatalf("expected properties type property, got none") } - if (*(*model.Properties)["properties"].Properties)["type"].IsAnyCovered { - t.Fatalf("expected properties type IsAnyCovered false, got true") + if !(*(*model.Properties)["properties"].Properties)["type"].IsAnyCovered { + t.Fatalf("expected properties type IsAnyCovered true, got false") } if (*model.Properties)["properties"].Discriminator == nil { @@ -1501,12 +1454,36 @@ func TestCoverage_DataFactoryLinkedServices(t *testing.T) { } func testCoverage(t *testing.T, tc testCase) (*coverage.Model, error) { - swaggerModel, err := coverage.GetModelInfoFromIndex( - tc.apiPath, - tc.apiVersion, - "", - ) + apiVersion := strings.Split(tc.resourceType, "@")[1] + + var swaggerModel *coverage.SwaggerModel + var err error + azureRepoDir := os.Getenv("AZURE_REST_REPO_DIR") + if azureRepoDir != "" { + if strings.HasSuffix(azureRepoDir, "specification") { + azureRepoDir += "/" + } + if !strings.HasSuffix(normarlizePath(azureRepoDir), "specification/") { + t.Fatalf("AZURE_REST_REPO_DIR must specify the specification folder, e.g., AZURE_REST_REPO_DIR=\"/home/test/go/src/github.com/azure/azure-rest-api-specs/specification/\"") + } + t.Logf("AZURE_REST_REPO_DIR: %s", azureRepoDir) + + swaggerModel, err = coverage.GetModelInfoFromLocalIndex( + tc.apiPath, + apiVersion, + tc.method, + azureRepoDir, + "./testdata/index.json", + ) + } else { + swaggerModel, err = coverage.GetModelInfoFromIndex( + tc.apiPath, + apiVersion, + tc.method, + "./testdata/index.json", + ) + } t.Logf("swaggerModel: %+v", swaggerModel) if err != nil { @@ -1518,17 +1495,17 @@ func testCoverage(t *testing.T, tc testCase) (*coverage.Model, error) { return nil, fmt.Errorf("expand model: %+v", err) } - //out, err := json.MarshalIndent(model, "", "\t") - //if err != nil { - // t.Error(err) - //} - //t.Logf("expand model %s", string(out)) + // out, err := json.MarshalIndent(model, "", "\t") + // if err != nil { + // t.Error(err) + // } + // t.Logf("expand model %s", string(out)) for _, rq := range tc.rawRequest { request := map[string]interface{}{} err = json.Unmarshal([]byte(rq), &request) if err != nil { - t.Error(err) + t.Errorf("error unmarshal request json: %v", err) } model.MarkCovered(request) @@ -1536,11 +1513,19 @@ func testCoverage(t *testing.T, tc testCase) (*coverage.Model, error) { model.CountCoverage() - //out, err = json.MarshalIndent(model, "", "\t") - //if err != nil { - // t.Error(err) - //} - //t.Logf("coverage model %s", string(out)) + if model.RootCoveredCount != tc.expectedCoveredCount { + t.Errorf("expected CoveredCount %d, got %d", tc.expectedCoveredCount, model.RootCoveredCount) + } + + if model.RootTotalCount != tc.expectedTotalCount { + t.Errorf("expected TotalCount %d, got %d", tc.expectedTotalCount, model.RootTotalCount) + } + + // out, err = json.MarshalIndent(model, "", "\t") + // if err != nil { + // t.Error(err) + // } + // t.Logf("coverage model %s", string(out)) coverageReport := coverage.CoverageReport{ Coverages: map[string]*coverage.CoverageItem{ @@ -1561,7 +1546,7 @@ func testCoverage(t *testing.T, tc testCase) (*coverage.Model, error) { }, } - storeCoverageReport(passReport, coverageReport, ".", fmt.Sprintf("test_coverage_report_%s.md", tc.name)) + storeCoverageReport(passReport, coverageReport, "./testdata/", fmt.Sprintf("test_coverage_report_%s.md", tc.name)) return model, nil } @@ -1578,9 +1563,11 @@ func storeCoverageReport(passReport types.PassReport, coverageReport coverage.Co } func testCredScan(t *testing.T, tc testCase) (*map[string]string, error) { + apiVersion := strings.Split(tc.resourceType, "@")[1] swaggerModel, err := coverage.GetModelInfoFromIndex( tc.apiPath, - tc.apiVersion, + apiVersion, + "PUT", "", ) @@ -1623,7 +1610,6 @@ func TestCredScan(t *testing.T) { tc := testCase{ name: "VM", resourceType: "Microsoft.Compute/virtualMachines@2023-03-01", - apiVersion: "2023-03-01", apiPath: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm", rawRequest: []string{ `{ diff --git a/coverage/expand.go b/coverage/expand.go index 18b3d50..a0af0e7 100644 --- a/coverage/expand.go +++ b/coverage/expand.go @@ -70,11 +70,20 @@ func getAllOfTable(swaggerPath string) (map[string]map[string]interface{}, error return allOfTable, nil } +func trimPath(path string) string { + if strings.Contains(path, "\\") { + return strings.ReplaceAll(path, "\\.\\", "\\") + } + return strings.ReplaceAll(path, "/./", "/") +} + func Expand(modelName, swaggerPath string) (*Model, error) { if modelName == "" { return nil, fmt.Errorf("modelName is empty") } + swaggerPath = trimPath(swaggerPath) + doc, err := loadSwagger(swaggerPath) if err != nil { return nil, err @@ -89,6 +98,8 @@ func Expand(modelName, swaggerPath string) (*Model, error) { output := expandSchema(modelSchema, swaggerPath, modelName, "#", spec, map[string]interface{}{}, map[string]interface{}{}) + output.IsRoot = true + return output, nil } @@ -178,6 +189,7 @@ func expandSchema(input openapiSpec.Schema, swaggerPath, modelName, identifier s properties[k] = v } } + output.ModelName = referenceModel.ModelName if referenceModel.Enum != nil { output.Enum = referenceModel.Enum } @@ -327,6 +339,7 @@ func SchemaNamePathFromRef(swaggerPath string, ref openapiSpec.Ref) (schemaName } else { swaggerPath, _ := filepath.Split(swaggerPath) schemaPath = swaggerPath + schemaPath + schemaPath = trimPath(schemaPath) } fragments := strings.Split(refUrl.Fragment, "/") diff --git a/coverage/expand_test.go b/coverage/expand_test.go index e84f72b..58b3f56 100644 --- a/coverage/expand_test.go +++ b/coverage/expand_test.go @@ -123,7 +123,7 @@ func TestExpandAll(t *testing.T) { t.Fatalf("AZURE_REST_REPO_DIR must specify the specification folder, e.g., AZURE_REST_REPO_DIR=\"/home/test/go/src/github.com/azure/azure-rest-api-specs/specification/\"") } - t.Logf("azure repo dir: %s", azureRepoDir) + t.Logf("AZURE_REST_REPO_DIR: %s", azureRepoDir) testResultFilePath := os.Getenv("TEST_RESULT_PATH") diff --git a/coverage/index.go b/coverage/index.go index b27d5b6..97538d2 100644 --- a/coverage/index.go +++ b/coverage/index.go @@ -165,7 +165,7 @@ type SwaggerModel struct { // GetModelInfoFromIndex will try to download online index from https://github.com/teowa/azure-rest-api-index-file, and get model info from it // if the index is already downloaded as in {indexFilePath}, it will use the cached index -func GetModelInfoFromIndex(resourceId, apiVersion, indexFilePath string) (*SwaggerModel, error) { +func GetModelInfoFromIndex(resourceId, apiVersion, method, indexFilePath string) (*SwaggerModel, error) { index, err := GetIndex(indexFilePath) if err != nil { return nil, err @@ -176,9 +176,9 @@ func GetModelInfoFromIndex(resourceId, apiVersion, indexFilePath string) (*Swagg if err != nil { return nil, fmt.Errorf("parsing URL %s: %+v", resourceURL, err) } - ref, err := index.Lookup("PUT", *uRL) + ref, err := index.Lookup(method, *uRL) if err != nil { - return nil, fmt.Errorf("lookup PUT URL %s in index: %+v", resourceURL, err) + return nil, fmt.Errorf("lookup %s URL %s in index: %+v", method, resourceURL, err) } model, err := GetModelInfoFromIndexRef(openapispec.Ref{Ref: *ref}, azureRepoURL) @@ -193,7 +193,7 @@ func GetModelInfoFromIndex(resourceId, apiVersion, indexFilePath string) (*Swagg } // GetModelInfoFromLocalIndex tries to build index from local swagger repo and get model info from it -func GetModelInfoFromLocalIndex(resourceId, apiVersion, swaggerRepo, indexCacheFile string) (*SwaggerModel, error) { +func GetModelInfoFromLocalIndex(resourceId, apiVersion, method, swaggerRepo, indexCacheFile string) (*SwaggerModel, error) { swaggerRepo, err := filepath.Abs(swaggerRepo) if err != nil { return nil, fmt.Errorf("swagger repo path %q is invalid: %+v", swaggerRepo, err) @@ -221,9 +221,9 @@ func GetModelInfoFromLocalIndex(resourceId, apiVersion, swaggerRepo, indexCacheF if err != nil { return nil, fmt.Errorf("parsing URL %s: %+v", resourceURL, err) } - ref, err := index.Lookup("PUT", *uRL) + ref, err := index.Lookup(method, *uRL) if err != nil { - return nil, fmt.Errorf("lookup PUT URL %s in index: %+v", resourceURL, err) + return nil, fmt.Errorf("lookup %s URL %s in index: %+v", method, resourceURL, err) } model, err := GetModelInfoFromIndexRef(openapispec.Ref{Ref: *ref}, swaggerRepo) @@ -309,7 +309,7 @@ func MockResourceIDFromType(azapiResourceType string) (string, string) { return fmt.Sprintf("%s/%s/providers/%s/%s", subscritionSeg, resourceGroupSeg, resourceProvider, typeIds), apiVersion } -func GetModelInfoFromIndexWithType(azapiResourceType, indexCacheFile string) (*SwaggerModel, error) { +func GetModelInfoFromIndexWithType(azapiResourceType, method, indexCacheFile string) (*SwaggerModel, error) { resourceId, apiVersion := MockResourceIDFromType(azapiResourceType) - return GetModelInfoFromIndex(resourceId, apiVersion, indexCacheFile) + return GetModelInfoFromIndex(resourceId, apiVersion, method, indexCacheFile) } diff --git a/coverage/index_test.go b/coverage/index_test.go index a70a1a0..88cd4e9 100644 --- a/coverage/index_test.go +++ b/coverage/index_test.go @@ -17,6 +17,7 @@ func TestGetModelInfoFromIndex_DataCollectionRule(t *testing.T) { swaggerModel, err := coverage.GetModelInfoFromIndex( "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/test-resources/providers/Microsoft.Insights/dataCollectionRules/testDCR", apiVersion, + "PUT", "", ) if err != nil { @@ -44,6 +45,7 @@ func TestGetModelInfoFromIndexWithCache_DataCollectionRule(t *testing.T) { swaggerModel, err := coverage.GetModelInfoFromIndex( "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/test-resources/providers/Microsoft.Insights/dataCollectionRules/testDCR", apiVersion, + "PUT", indexFilePath, ) if err != nil { @@ -71,6 +73,7 @@ func TestGetModelInfoFromIndex_DeviceSecurityGroups(t *testing.T) { swaggerModel, err := coverage.GetModelInfoFromIndex( "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/SampleRG/providers/Microsoft.Devices/iotHubs/sampleiothub/providers/Microsoft.Security/deviceSecurityGroups/samplesecuritygroup", apiVersion, + "PUT", "", ) if err != nil { @@ -95,7 +98,7 @@ func TestGetModelInfoFromIndex_DeviceSecurityGroups(t *testing.T) { func TestGetModelInfoFromIndexWithType_DataCollectionRule(t *testing.T) { azapiResourceType := "Microsoft.Insights/dataCollectionRules@2022-06-01" - swaggerModel, err := coverage.GetModelInfoFromIndexWithType(azapiResourceType, "") + swaggerModel, err := coverage.GetModelInfoFromIndexWithType(azapiResourceType, "PUT", "") if err != nil { t.Fatalf("get model info from index error: %+v", err) } @@ -108,7 +111,7 @@ func TestGetModelInfoFromIndexWithType_DataCollectionRule(t *testing.T) { func TestGetModelInfoFromIndexWithTypeWithCache_DataCollectionRule(t *testing.T) { azapiResourceType := "Microsoft.Insights/dataCollectionRules@2022-06-01" - swaggerModel, err := coverage.GetModelInfoFromIndexWithType(azapiResourceType, indexFilePath) + swaggerModel, err := coverage.GetModelInfoFromIndexWithType(azapiResourceType, "PUT", indexFilePath) if err != nil { t.Fatalf("get model info from index error: %+v", err) } @@ -118,7 +121,7 @@ func TestGetModelInfoFromIndexWithTypeWithCache_DataCollectionRule(t *testing.T) t.Fatalf("expected apiPath %s, got %s", expectedApiPath, swaggerModel.ApiPath) } - _, err = coverage.GetModelInfoFromIndexWithType(azapiResourceType, indexFilePath) + _, err = coverage.GetModelInfoFromIndexWithType(azapiResourceType, "PUT", indexFilePath) if err != nil { t.Fatalf("get model info from index error: %+v", err) } @@ -126,7 +129,7 @@ func TestGetModelInfoFromIndexWithTypeWithCache_DataCollectionRule(t *testing.T) func TestGetModelInfoFromIndexWithType_DeviceSecurityGroups(t *testing.T) { azapiResourceType := "Microsoft.Security/deviceSecurityGroups@2019-08-01" - swaggerModel, err := coverage.GetModelInfoFromIndexWithType(azapiResourceType, "") + swaggerModel, err := coverage.GetModelInfoFromIndexWithType(azapiResourceType, "PUT", "") if err != nil { t.Fatalf("get model info from index error: %+v", err) } @@ -147,6 +150,7 @@ func TestGetModelInfoFromLocalIndex_DataCollectionRule(t *testing.T) { swaggerModel, err := coverage.GetModelInfoFromLocalIndex( "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/test-resources/providers/Microsoft.Insights/dataCollectionRules/testDCR", apiVersion, + "PUT", azureRepoDir, "", ) @@ -180,6 +184,7 @@ func TestGetModelInfoFromLocalIndexWithCache_DataCollectionRule(t *testing.T) { swaggerModel, err := coverage.GetModelInfoFromLocalIndex( "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/test-resources/providers/Microsoft.Insights/dataCollectionRules/testDCR", apiVersion, + "PUT", azureRepoDir, indexFilePath, ) @@ -205,6 +210,7 @@ func TestGetModelInfoFromLocalIndexWithCache_DataCollectionRule(t *testing.T) { _, err = coverage.GetModelInfoFromLocalIndex( "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/test-resources/providers/Microsoft.Insights/dataCollectionRules/testDCR", apiVersion, + "PUT", azureRepoDir, indexFilePath, ) diff --git a/coverage/report.go b/coverage/report.go index 421ddd6..e4acf8a 100644 --- a/coverage/report.go +++ b/coverage/report.go @@ -33,7 +33,7 @@ func (c *CoverageReport) AddCoverageFromState(resourceId, resourceType string, j swaggerModel = swaggerModelFromLocal } if swaggerModel == nil { - swaggerModelFromIndex, err := GetModelInfoFromIndex(resourceId, apiVersion, "") + swaggerModelFromIndex, err := GetModelInfoFromIndex(resourceId, apiVersion, "PUT", "") if err != nil { return fmt.Errorf("error find the path for %s from index: %+v", resourceId, err) } @@ -79,7 +79,7 @@ ${coverage_details} if v.Model.IsFullyCovered { fullyCoveredPath = append(fullyCoveredPath, v.DisplayName) } else { - partiallyCoveredPath = append(partiallyCoveredPath, fmt.Sprintf("%v (%v/%v)", v.DisplayName, v.Model.CoveredCount, v.Model.TotalCount)) + partiallyCoveredPath = append(partiallyCoveredPath, fmt.Sprintf("%v (%v/%v)", v.DisplayName, v.Model.RootCoveredCount, v.Model.RootTotalCount)) } } @@ -98,7 +98,7 @@ ${coverage_details} for _, v := range c.Coverages { count++ - reportDetail := getReport(v.Model) + reportDetail := getReport(v.Model.ModelName, v.Model) sort.Strings(reportDetail) coverages = append(coverages, fmt.Sprintf(`##### @@ -107,20 +107,14 @@ ${coverage_details} [swagger](%[3]v)
-
-%[5]v(%[6]v/%[7]v) -
-%[8]v - -
-
+%[4]v
--- -`, v.DisplayName, v.ApiPath, v.Model.SourceFile, getStyle(v.Model.IsFullyCovered), v.Model.ModelName, v.Model.CoveredCount, v.Model.TotalCount, strings.Join(reportDetail, "\n\n"))) +`, v.DisplayName, v.ApiPath, v.Model.SourceFile, strings.Join(reportDetail, "\n\n"))) } sort.Strings(coverages) @@ -142,11 +136,11 @@ func (c *CoverageReport) MarkdownContentCompact() string { for _, v := range c.Coverages { coverage := 100.0 if v.Model.TotalCount > 0 { - coverage = float64(v.Model.CoveredCount * 100 / v.Model.TotalCount) + coverage = float64(v.Model.RootCoveredCount * 100 / v.Model.RootTotalCount) } - content += fmt.Sprintf("|%s|%d|%d|%.1f%%|\n", v.DisplayName, v.Model.CoveredCount, v.Model.TotalCount, coverage) - total += v.Model.TotalCount - covered += v.Model.CoveredCount + content += fmt.Sprintf("|%s|%d|%d|%.1f%%|\n", v.DisplayName, v.Model.RootCoveredCount, v.Model.RootTotalCount, coverage) + total += v.Model.RootTotalCount + covered += v.Model.RootCoveredCount } coverage := 100.0 @@ -158,8 +152,21 @@ func (c *CoverageReport) MarkdownContentCompact() string { return template + content } -func getReport(model *Model) []string { +func getReport(displayName string, model *Model) []string { out := make([]string, 0) + style := getStyle(model.IsFullyCovered) + + if hasNoDetail(model) { + // leaf property + out = append(out, + fmt.Sprintf(` +
+%[3]v + +
`, model.Identifier, style, displayName), + ) + return out + } if isBoolEnumDisplayed { if model.Enum != nil { @@ -178,7 +185,7 @@ func getReport(model *Model) []string { } if model.Item != nil { - return getReport(model.Item) + return getReport(displayName, model.Item) } if model.Properties != nil { @@ -196,17 +203,16 @@ func getReport(model *Model) []string { if v.VariantType != nil { variantType = *v.VariantType } - variantKey := fmt.Sprintf("%s{%s}", k, variantType) - - out = append(out, getChildReport(variantKey, v)) + variantKey := getDiscriminatorKey(k, variantType) + out = append(out, getReport(variantKey, v)...) for variantType, variant := range *v.Variants { variantType := variantType if variant.VariantType != nil { variantType = *variant.VariantType } - variantKey := fmt.Sprintf("%s{%s}", k, variantType) - out = append(out, getChildReport(variantKey, variant)) + variantKey := getDiscriminatorKey(k, variantType) + out = append(out, getReport(variantKey, variant)...) } continue } @@ -216,25 +222,68 @@ func getReport(model *Model) []string { if v.Item.VariantType != nil { variantType = *v.Item.VariantType } - variantKey := fmt.Sprintf("%s{%s}", k, variantType) - out = append(out, getChildReport(variantKey, v)) + variantKey := getDiscriminatorKey(k, variantType) + out = append(out, getReport(variantKey, v)...) for variantType, variant := range *v.Item.Variants { variantType := variantType if variant.VariantType != nil { variantType = *variant.VariantType } - variantKey := fmt.Sprintf("%s{%s}", k, variantType) - out = append(out, getChildReport(variantKey, variant)) + variantKey := getDiscriminatorKey(k, variantType) + out = append(out, getReport(variantKey, variant)...) } continue } - out = append(out, getChildReport(k, v)) + out = append(out, getReport(k, v)...) } } - return out + sort.Strings(out) + + outWithoutVariant := []string{ + fmt.Sprintf(` +
+%[3]v %[4]v +
+ +%[5]v + +
+
`, model.Identifier, style, displayName, getCoverageCount(model), strings.Join(out, "\n\n")), + } + + if model.IsRoot { + var variants *map[string]*Model + if model.Variants != nil { + variants = model.Variants + } + if model.Item != nil && model.Item.Variants != nil { + variants = model.Item.Variants + } + + if variants != nil { + outWithVariant := make([]string, 0) + outWithVariant = append(outWithVariant, outWithoutVariant...) + + for variantType, variant := range *variants { + variantType := variantType + if variant.VariantType != nil { + variantType = *variant.VariantType + } + variantKey := getDiscriminatorKey(displayName, variantType) + outWithVariant = append(outWithVariant, getReport(variantKey, variant)...) + } + + sort.Strings(outWithVariant) + + return outWithVariant + } + } + + return outWithoutVariant + } func getEnumBoolReport(name string, isCovered bool) string { @@ -251,35 +300,6 @@ func getCoverageCount(model *Model) string { return fmt.Sprintf("(%v/%v)", model.CoveredCount, model.TotalCount) } -func getChildReport(name string, model *Model) string { - var style, report string - - style = getStyle(model.IsFullyCovered) - - if hasNoDetail(model) { - // leaf property - report = fmt.Sprintf(` -
-%[3]v - -
`, model.Identifier, style, name) - } else { - childReport := getReport(model) - sort.Strings(childReport) - report = fmt.Sprintf(` -
-%[3]v %[4]v -
- -%[5]v - -
-
`, model.Identifier, style, name, getCoverageCount(model), strings.Join(childReport, "\n\n")) - } - - return report -} - func hasNoDetail(model *Model) bool { if model.Properties == nil && model.Variants == nil && model.Item == nil && (!isBoolEnumDisplayed || (model.Bool == nil && model.Enum == nil)) { return true @@ -299,3 +319,7 @@ func getStyle(isFullyCovered bool) string { } return " style=\"color:red\"" } + +func getDiscriminatorKey(modelName, variantType string) string { + return fmt.Sprintf("%s{%s}", modelName, variantType) +}