diff --git a/gen/utils/data/class.go b/gen/utils/data/class.go index a16ba3882..8e69c2905 100644 --- a/gen/utils/data/class.go +++ b/gen/utils/data/class.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils" - "github.com/CiscoDevNet/terraform-provider-aci/v2/internal/provider" ) type Class struct { @@ -21,8 +20,9 @@ type Class struct { ClassDefinition ClassDefinition // Deprecated resources include a warning the resource and datasource schemas. Deprecated bool - // The APIC versions in which the class is deprecated. - DeprecatedVersions []VersionRange + // The deprecated APIC versions for the class. + // Used to indicate versions where the class is deprecated but still functional. + DeprecatedVersions *Versions // Documentation specific information for the class. Documentation ClassDocumentation // List of all identifying properties of the class. @@ -64,10 +64,8 @@ type Class struct { // The relative name (RN) format of the class, ex "tn-{name}". RnFormat string // The supported APIC versions for the class. - // Each version range is separated by a comma, ex "4.2(7f)-4.2(7w),5.2(1g)-". - // The first version is the minimum version and the second version is the maximum version. - // A dash at the end of a range (ex. 4.2(7f)-) indicates that the class is supported from the first version to the latest version. - Versions []VersionRange + // Parsed from the "versions" field in the meta file (e.g., "1.0(1e)-", "4.2(7f)-4.2(7w),5.2(1g)-"). + SupportedVersions *Versions } type PlatformTypeEnum int @@ -133,18 +131,6 @@ type ClassDocumentation struct { Warnings []string } -type VersionRange struct { - // The maximum version of the range. - // This is the second version of the range. - // The version is in the format "4.2(7w)". - // A dash at the end of a range (ex. 4.2(7f)-) indicates that the class is supported from the first version to the latest version. - Max provider.Version - // The minimum version of the range. - // This is the first version of the range. - // The version is in the format "4.2(7f)". - Min provider.Version -} - func NewClass(className string, ds *DataStore) (*Class, error) { genLogger.Trace(fmt.Sprintf("Creating new class struct with class name: %s.", className)) @@ -214,8 +200,10 @@ func (c *Class) setClassData(ds *DataStore) error { // TODO: add placeholder function for Deprecated c.setDeprecated() - // TODO: add placeholder function for DeprecatedVersions - c.setDeprecatedVersions() + err = c.setDeprecatedVersions() + if err != nil { + return err + } // TODO: add function to set Documentation c.setDocumentation() @@ -256,8 +244,10 @@ func (c *Class) setClassData(ds *DataStore) error { // TODO: add function to set RnFormat c.setRnFormat() - // TODO: add function to set Versions - c.setVersions() + err = c.setSupportedVersions() + if err != nil { + return err + } genLogger.Debug(fmt.Sprintf("Successfully set class data for class '%s'.", c.Name)) return nil @@ -324,10 +314,25 @@ func (c *Class) setDeprecated() { genLogger.Debug(fmt.Sprintf("Successfully set Deprecated for class '%s'.", c.Name)) } -func (c *Class) setDeprecatedVersions() { - // Determine the APIC versions in which the class is deprecated. +func (c *Class) setDeprecatedVersions() error { + // Determine the deprecated APIC versions for the class from the definition file. genLogger.Debug(fmt.Sprintf("Setting DeprecatedVersions for class '%s'.", c.Name)) - genLogger.Debug(fmt.Sprintf("Successfully set DeprecatedVersions for class '%s'.", c.Name)) + + // DeprecatedVersions is only set from the definition file (meta file doesn't support this yet). + metaVersions := c.ClassDefinition.DeprecatedVersions + if metaVersions == "" { + genLogger.Debug(fmt.Sprintf("No DeprecatedVersions specified for class '%s'.", c.Name)) + return nil + } + + versions, err := NewVersions(metaVersions) + if err != nil { + return fmt.Errorf("failed to parse deprecated versions for class '%s': %w", c.Name, err) + } + c.DeprecatedVersions = versions + + genLogger.Debug(fmt.Sprintf("Successfully set DeprecatedVersions for class '%s'. Versions: '%s'", c.Name, c.DeprecatedVersions)) + return nil } func (c *Class) setDocumentation() { @@ -511,10 +516,29 @@ func (c *Class) setRnFormat() { genLogger.Debug(fmt.Sprintf("Successfully set RnFormat for class '%s'.", c.Name)) } -func (c *Class) setVersions() { +func (c *Class) setSupportedVersions() error { // Determine the supported APIC versions for the class. - genLogger.Debug(fmt.Sprintf("Setting Versions for class '%s'.", c.Name)) - genLogger.Debug(fmt.Sprintf("Successfully set Versions for class '%s'.", c.Name)) + genLogger.Debug(fmt.Sprintf("Setting SupportedVersions for class '%s'.", c.Name)) + + // Initialize with versions from ClassDefinition, if not defined set the versions from meta file. + metaVersions := c.ClassDefinition.SupportedVersions + if metaVersions == "" { + metaVersions, _ = c.MetaFileContent["versions"].(string) + } + + // When versions are not specified error to force users to add versions. + if metaVersions == "" { + return fmt.Errorf("versions not specified for class '%s': add versions to the class definition file", c.Name) + } + + versions, err := NewVersions(metaVersions) + if err != nil { + return fmt.Errorf("failed to parse versions for class '%s': %w", c.Name, err) + } + c.SupportedVersions = versions + + genLogger.Debug(fmt.Sprintf("Successfully set SupportedVersions for class '%s'. Versions: '%s'", c.Name, c.SupportedVersions)) + return nil } func getRelationshipResourceName(ds *DataStore, toClass string) string { diff --git a/gen/utils/data/class_test.go b/gen/utils/data/class_test.go index fb650ec6e..9b3970b49 100644 --- a/gen/utils/data/class_test.go +++ b/gen/utils/data/class_test.go @@ -7,7 +7,6 @@ import ( "github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils/test" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // testClassName creates a ClassName for testing purposes. @@ -39,7 +38,7 @@ func TestSetResourceNameFromLabelNoRelationWithIdentifier(t *testing.T) { err := class.setResourceName(ds) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Equal(t, "annotation", class.ResourceName, test.MessageEqual("annotation", class.ResourceName, t.Name())) assert.Equal(t, "annotations", class.ResourceNameNested, test.MessageEqual("annotations", class.ResourceNameNested, t.Name())) } @@ -63,10 +62,10 @@ func TestSetResourceNameFromLabelNoRelationWithoutIdentifier(t *testing.T) { } err := class.setRelation() - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) err = class.setResourceName(ds) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Equal(t, "relation_to_vrf", class.ResourceName, test.MessageEqual("relation_to_vrf", class.ResourceName, t.Name())) assert.Equal(t, "relation_to_vrf", class.ResourceNameNested, test.MessageEqual("relation_to_vrf", class.ResourceNameNested, t.Name())) } @@ -90,10 +89,10 @@ func TestSetResourceNameToRelation(t *testing.T) { } err := class.setRelation() - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) err = class.setResourceName(ds) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Equal(t, "relation_to_contract", class.ResourceName, test.MessageEqual("relation_to_contract", class.ResourceName, t.Name())) assert.Equal(t, "relation_to_contracts", class.ResourceNameNested, test.MessageEqual("relation_to_contracts", class.ResourceNameNested, t.Name())) } @@ -119,10 +118,10 @@ func TestSetResourceNameFromToRelation(t *testing.T) { } err := class.setRelation() - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) err = class.setResourceName(ds) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Equal(t, "relation_from_netflow_exporter_policy_to_vrf", class.ResourceName, test.MessageEqual("relation_from_netflow_exporter_policy_to_vrf", class.ResourceName, t.Name())) assert.Equal(t, "relation_to_vrf", class.ResourceNameNested, test.MessageEqual("relation_to_vrf", class.ResourceNameNested, t.Name())) } @@ -256,14 +255,14 @@ func TestSetRelation(t *testing.T) { err := class.setRelation() if expected.Error { - require.Error(t, err) + assert.Error(t, err) if expected.ErrorMsg != "" { assert.ErrorContains(t, err, expected.ErrorMsg) } return } - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Equal(t, expected.FromClass, class.Relation.FromClass, test.MessageEqual(expected.FromClass, class.Relation.FromClass, testCase.Name)) assert.Equal(t, expected.ToClass, class.Relation.ToClass, test.MessageEqual(expected.ToClass, class.Relation.ToClass, testCase.Name)) assert.Equal(t, expected.Type, class.Relation.Type, test.MessageEqual(expected.Type, class.Relation.Type, testCase.Name)) @@ -614,7 +613,7 @@ func TestSetChildren(t *testing.T) { } err := class.setChildren(ds) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) if len(expected) == 0 { assert.Empty(t, class.Children, test.MessageEqual(expected, class.Children, testCase.Name)) @@ -656,7 +655,7 @@ func TestSetChildrenWarnsWhenClassInBothIncludeAndExclude(t *testing.T) { } err := class.setChildren(ds) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) // Verify the warning was logged. logOutput := logBuffer.String() @@ -877,7 +876,7 @@ func TestSetParents(t *testing.T) { } err := class.setParents() - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) if len(expected) == 0 { assert.Empty(t, class.Parents, test.MessageEqual(expected, class.Parents, testCase.Name)) @@ -913,7 +912,7 @@ func TestSetParentsWarnsWhenClassInBothIncludeAndExclude(t *testing.T) { } err := class.setParents() - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) // Verify the warning was logged. logOutput := logBuffer.String() @@ -1071,3 +1070,269 @@ func TestSortAndConvertToClassNames(t *testing.T) { }) } } + +type setSupportedVersionsInput struct { + ClassDefinitionVersions string + MetaVersions interface{} +} + +type setSupportedVersionsExpected struct { + Raw string + String string + Error bool + ErrorMsg string +} + +func TestSetSupportedVersions(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + testCases := []test.TestCase{ + { + Name: "test_versions_from_meta_file", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "", + MetaVersions: "4.2(7f)-", + }, + Expected: setSupportedVersionsExpected{ + Raw: "4.2(7f)-", + String: "4.2(7f) and later", + }, + }, + { + Name: "test_versions_from_class_definition_override", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "5.0(1a)-", + MetaVersions: "4.2(7f)-", + }, + Expected: setSupportedVersionsExpected{ + Raw: "5.0(1a)-", + String: "5.0(1a) and later", + }, + }, + { + Name: "test_versions_from_class_definition_when_meta_empty", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "5.0(1a)-", + MetaVersions: "", + }, + Expected: setSupportedVersionsExpected{ + Raw: "5.0(1a)-", + String: "5.0(1a) and later", + }, + }, + { + Name: "test_versions_from_class_definition_when_meta_nil", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "5.0(1a)-", + MetaVersions: nil, + }, + Expected: setSupportedVersionsExpected{ + Raw: "5.0(1a)-", + String: "5.0(1a) and later", + }, + }, + { + Name: "test_multiple_ranges", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "", + MetaVersions: "3.2(10e)-3.2(10g),4.2(7f)-", + }, + Expected: setSupportedVersionsExpected{ + Raw: "3.2(10e)-3.2(10g),4.2(7f)-", + String: "3.2(10e) to 3.2(10g), 4.2(7f) and later", + }, + }, + { + Name: "test_multiple_ranges_sorted", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "", + MetaVersions: "5.2(1g)-,3.2(10e)-3.2(10g)", + }, + Expected: setSupportedVersionsExpected{ + Raw: "5.2(1g)-,3.2(10e)-3.2(10g)", + String: "3.2(10e) to 3.2(10g), 5.2(1g) and later", + }, + }, + { + Name: "test_error_empty_versions", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "", + MetaVersions: "", + }, + Expected: setSupportedVersionsExpected{ + Error: true, + ErrorMsg: "versions not specified for class 'fvTenant': add versions to the class definition file", + }, + }, + { + Name: "test_error_nil_versions", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "", + MetaVersions: nil, + }, + Expected: setSupportedVersionsExpected{ + Error: true, + ErrorMsg: "versions not specified for class 'fvTenant': add versions to the class definition file", + }, + }, + { + Name: "test_error_invalid_version", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "", + MetaVersions: "invalid", + }, + Expected: setSupportedVersionsExpected{ + Error: true, + ErrorMsg: "failed to parse versions for class 'fvTenant': invalid version 'invalid': unknown", + }, + }, + { + Name: "test_error_invalid_version_in_range", + Input: setSupportedVersionsInput{ + ClassDefinitionVersions: "", + MetaVersions: "4.2(7f)-,invalid", + }, + Expected: setSupportedVersionsExpected{ + Error: true, + ErrorMsg: "failed to parse versions for class 'fvTenant': invalid version 'invalid': unknown", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + input := testCase.Input.(setSupportedVersionsInput) + expected := testCase.Expected.(setSupportedVersionsExpected) + + class := Class{ + Name: testClassName("fvTenant"), + ClassDefinition: ClassDefinition{ + SupportedVersions: input.ClassDefinitionVersions, + }, + MetaFileContent: map[string]interface{}{}, + } + if input.MetaVersions != nil { + class.MetaFileContent["versions"] = input.MetaVersions + } + + err := class.setSupportedVersions() + + if expected.Error { + assert.EqualError(t, err, expected.ErrorMsg) + } else { + assert.NoError(t, err, test.MessageUnexpectedError(err)) + assert.Equal(t, expected.Raw, class.SupportedVersions.Raw(), test.MessageEqual(expected.Raw, class.SupportedVersions.Raw(), testCase.Name)) + assert.Equal(t, expected.String, class.SupportedVersions.String(), test.MessageEqual(expected.String, class.SupportedVersions.String(), testCase.Name)) + } + }) + } +} + +type setDeprecatedVersionsInput struct { + ClassDefinitionVersions string +} + +type setDeprecatedVersionsExpected struct { + Raw string + String string + Nil bool + Error bool + ErrorMsg string +} + +func TestSetDeprecatedVersions(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + testCases := []test.TestCase{ + { + Name: "test_deprecated_versions_not_set", + Input: setDeprecatedVersionsInput{ + ClassDefinitionVersions: "", + }, + Expected: setDeprecatedVersionsExpected{ + Nil: true, + }, + }, + { + Name: "test_deprecated_versions_single_range", + Input: setDeprecatedVersionsInput{ + ClassDefinitionVersions: "4.2(7f)-", + }, + Expected: setDeprecatedVersionsExpected{ + Raw: "4.2(7f)-", + String: "4.2(7f) and later", + }, + }, + { + Name: "test_deprecated_versions_bounded_range", + Input: setDeprecatedVersionsInput{ + ClassDefinitionVersions: "3.2(10e)-4.2(7f)", + }, + Expected: setDeprecatedVersionsExpected{ + Raw: "3.2(10e)-4.2(7f)", + String: "3.2(10e) to 4.2(7f)", + }, + }, + { + Name: "test_deprecated_versions_multiple_ranges", + Input: setDeprecatedVersionsInput{ + ClassDefinitionVersions: "3.2(10e)-3.2(10g),4.2(7f)-", + }, + Expected: setDeprecatedVersionsExpected{ + Raw: "3.2(10e)-3.2(10g),4.2(7f)-", + String: "3.2(10e) to 3.2(10g), 4.2(7f) and later", + }, + }, + { + Name: "test_error_invalid_deprecated_version", + Input: setDeprecatedVersionsInput{ + ClassDefinitionVersions: "invalid", + }, + Expected: setDeprecatedVersionsExpected{ + Error: true, + ErrorMsg: "failed to parse deprecated versions for class 'fvTenant': invalid version 'invalid': unknown", + }, + }, + { + Name: "test_error_invalid_deprecated_version_in_range", + Input: setDeprecatedVersionsInput{ + ClassDefinitionVersions: "4.2(7f)-,invalid", + }, + Expected: setDeprecatedVersionsExpected{ + Error: true, + ErrorMsg: "failed to parse deprecated versions for class 'fvTenant': invalid version 'invalid': unknown", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + input := testCase.Input.(setDeprecatedVersionsInput) + expected := testCase.Expected.(setDeprecatedVersionsExpected) + + class := Class{ + Name: testClassName("fvTenant"), + ClassDefinition: ClassDefinition{ + DeprecatedVersions: input.ClassDefinitionVersions, + }, + } + + err := class.setDeprecatedVersions() + + if expected.Error { + assert.EqualError(t, err, expected.ErrorMsg) + } else if expected.Nil { + assert.NoError(t, err, test.MessageUnexpectedError(err)) + assert.Nil(t, class.DeprecatedVersions, "expected DeprecatedVersions to be nil") + } else { + assert.NoError(t, err, test.MessageUnexpectedError(err)) + assert.Equal(t, expected.Raw, class.DeprecatedVersions.Raw(), test.MessageEqual(expected.Raw, class.DeprecatedVersions.Raw(), testCase.Name)) + assert.Equal(t, expected.String, class.DeprecatedVersions.String(), test.MessageEqual(expected.String, class.DeprecatedVersions.String(), testCase.Name)) + } + }) + } +} diff --git a/gen/utils/data/data_test.go b/gen/utils/data/data_test.go index 231649959..db376089e 100644 --- a/gen/utils/data/data_test.go +++ b/gen/utils/data/data_test.go @@ -7,7 +7,6 @@ import ( "github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils/test" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -71,7 +70,7 @@ func TestLoadClass(t *testing.T) { if expected.Error { assert.Error(t, err) } else { - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Contains(t, ds.Classes, testCase.Input.(string), test.MessageContains(ds.Classes, testCase.Input.(string), testCase.Name)) } }) @@ -92,7 +91,7 @@ func TestLoadClassAlreadyLoaded(t *testing.T) { // Loading the same class should not error and should skip err := ds.loadClass("fvTenant") - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Len(t, ds.Classes, 1) } @@ -180,7 +179,7 @@ func TestRetrieveEnvMetaClassesFromRemote(t *testing.T) { assert.Error(t, err) } else if input.EnvValue == "" { // Empty env should not error - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) } _ = tempDir // Used for cleanup @@ -199,7 +198,7 @@ func TestRetrieveEnvMetaClassesFromRemoteEmptyEnv(t *testing.T) { err := ds.retrieveEnvMetaClassesFromRemote() - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) } type refreshMetaFilesExpected struct { @@ -248,7 +247,7 @@ func TestRefreshMetaFiles(t *testing.T) { if expected.Error { assert.Error(t, err) } else { - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) } }) } @@ -312,7 +311,7 @@ func TestRetrieveMetaFileFromRemote(t *testing.T) { if expected.Error { assert.Error(t, err) } else { - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) } }) } @@ -329,5 +328,5 @@ func TestRetrieveMetaFileFromRemoteAlreadyRetrieved(t *testing.T) { // Should skip retrieval and not error err := ds.retrieveMetaFileFromRemote("fvTenant") - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) } diff --git a/gen/utils/data/definitions.go b/gen/utils/data/definitions.go index 93ec7b699..28b02167a 100644 --- a/gen/utils/data/definitions.go +++ b/gen/utils/data/definitions.go @@ -34,6 +34,9 @@ type ClassDefinition struct { // Overrides the default deletion behavior from meta file. Set to "never" to prevent deletion of the class. // The value "never" is used to keep the input consistent with the meta data file. AllowDelete string `yaml:"allow_delete"` + // The deprecated APIC versions for the class. Format: "1.0(1e)-" or "4.2(7f)-4.2(7w),5.2(1g)-". + // Used to indicate versions where the class is deprecated but still functional. + DeprecatedVersions string `yaml:"deprecated_versions"` // A list of child class names to exclude from the Children list. ExcludeChildren []string `yaml:"exclude_children"` // A list of parent class names to exclude from the Parents list. @@ -42,6 +45,8 @@ type ClassDefinition struct { IncludeChildren []string `yaml:"include_children"` // A list of parent class names to include in the Parents list outside of the standard inclusion logic. IncludeParents []string `yaml:"include_parents"` + // Overrides the versions from the meta file. Format: "1.0(1e)-" or "4.2(7f)-4.2(7w),5.2(1g)-". + SupportedVersions string `yaml:"supported_versions"` } func loadClassDefinition(className string) ClassDefinition { diff --git a/gen/utils/data/property.go b/gen/utils/data/property.go index dae49f38c..66c085646 100644 --- a/gen/utils/data/property.go +++ b/gen/utils/data/property.go @@ -13,8 +13,6 @@ type Property struct { // Indicates if the property is deprecated in the resource and datasource schemas. // Deprecated properties include a warning the resource and datasource schemas. Deprecated bool - // The APIC versions in which the property is deprecated. - DeprecatedVersions []VersionRange // Documentation specific information for the property. Documentation PropertyDocumentation // Migration specific information for the property. @@ -30,6 +28,12 @@ type Property struct { ReadOnly bool // Indicates if the property is required in the resource and datasource schemas. Required bool + // The supported APIC versions for the property. + // Each version range is separated by a comma, ex "4.2(7f)-4.2(7w),5.2(1g)-". + // The first version is the minimum version and the second version is the maximum version. + // A dash at the end of a range (ex. 4.2(7f)-) indicates that the class is supported from the first version to the latest version. + // TODO: Add DeprecatedVersions field when meta file exposes deprecation information. + SupportedVersions *Versions // Test specific information for the property. // This is used to generate the test cases and examples for the property. // TODO: re-evaluate the structure when creating example and test templates. @@ -42,11 +46,6 @@ type Property struct { ValidValues []ValidValue // The ValueTypeEnum type is used to indicate the type of the property in the resource and datasource schemas. ValueType ValueTypeEnum - // The supported APIC versions for the property. - // Each version range is separated by a comma, ex "4.2(7f)-4.2(7w),5.2(1g)-". - // The first version is the minimum version and the second version is the maximum version. - // A dash at the end of a range (ex. 4.2(7f)-) indicates that the class is supported from the first version to the latest version. - Versions []VersionRange } type MigrationValue struct { diff --git a/gen/utils/data/version.go b/gen/utils/data/version.go new file mode 100644 index 000000000..4b6fba7e2 --- /dev/null +++ b/gen/utils/data/version.go @@ -0,0 +1,238 @@ +package data + +import ( + "fmt" + "slices" + "strings" + + "github.com/CiscoDevNet/terraform-provider-aci/v2/internal/provider" +) + +type VersionRange struct { + // raw contains the original version range string from the meta file. + raw string + // The minimum version of the range. + // The version is in the format "4.2(7f)". + // When nil, there is no lower bound. + min *provider.Version + // The maximum version of the range. + // The version is in the format "4.2(7w)". + // When nil, there is no upper bound (unlimited). + max *provider.Version +} + +func (vr *VersionRange) Raw() string { + // Raw returns the original version range string from the meta file. + return vr.raw +} + +func (vr *VersionRange) Min() *provider.Version { + // Min returns the minimum version of the range, or nil if there is no lower bound. + return vr.min +} + +func (vr *VersionRange) Max() *provider.Version { + // Max returns the maximum version of the range, or nil if there is no upper bound. + return vr.max +} + +func (vr *VersionRange) IsSingleVersion() bool { + // IsSingleVersion returns true if min and max are equal (representing a single version, not a range). + return vr.min != nil && vr.max != nil && *vr.min == *vr.max +} + +func (vr *VersionRange) String() string { + // String returns the version range as a documentation-friendly string. + // Examples: + // - Single version: "4.2(7f)" + // - Bounded range: "4.2(7f) to 4.2(7w)" + // - Unbounded upper: "4.2(7f) and later" + // - Unbounded lower: "up to 4.2(7w)" + switch { + case vr.IsSingleVersion(): + return vr.min.String() + case vr.min == nil: + return fmt.Sprintf("up to %s", vr.max) + case vr.max == nil: + return fmt.Sprintf("%s and later", vr.min) + default: + return fmt.Sprintf("%s to %s", vr.min, vr.max) + } +} + +func NewVersionRange(metaVersionRange string) (*VersionRange, error) { + // NewVersionRange parses a single version range string and returns a VersionRange struct. + // Examples: + // - "1.0(1e)" -> single version (min == max) + // - "4.2(7f)-4.2(7w)" -> bounded range + // - "4.2(7f)-" -> unbounded range (max is nil) + // - "-4.2(7w)" -> unbounded range (min is nil) + genLogger.Trace(fmt.Sprintf("Parsing version range: %s", metaVersionRange)) + + minVersion, maxVersion, err := parseVersionBounds(metaVersionRange) + if err != nil { + return nil, err + } + + versionRange := &VersionRange{raw: metaVersionRange, min: minVersion, max: maxVersion} + + genLogger.Trace(fmt.Sprintf("Successfully parsed version range '%s': %s", metaVersionRange, versionRange)) + return versionRange, nil +} + +func parseVersionBounds(metaVersionRange string) (*provider.Version, *provider.Version, error) { + // parseVersionBounds extracts min and max versions from a version range string. + // Returns (minVersion, maxVersion, error). + var minVersion, maxVersion *provider.Version + + if strings.Contains(metaVersionRange, "-") { + // SplitN with n=2 ensures we only split on the first dash. + // Extra dashes remain in the second part and will fail ParseVersion validation. + metaVersionParts := strings.SplitN(metaVersionRange, "-", 2) + minMetaVersion := metaVersionParts[0] + maxMetaVersion := metaVersionParts[1] + + // Parse minimum version if present + if minMetaVersion != "" { + minVersionResult := provider.ParseVersion(minMetaVersion) + if minVersionResult.Error != "" { + return nil, nil, fmt.Errorf("invalid minimum version '%s': %s", minMetaVersion, minVersionResult.Error) + } + minVersion = minVersionResult.Version + } + + // Parse maximum version if present + if maxMetaVersion != "" { + maxVersionResult := provider.ParseVersion(maxMetaVersion) + if maxVersionResult.Error != "" { + return nil, nil, fmt.Errorf("invalid maximum version '%s': %s", maxMetaVersion, maxVersionResult.Error) + } + maxVersion = maxVersionResult.Version + } + } else { + versionResult := provider.ParseVersion(metaVersionRange) + if versionResult.Error != "" { + return nil, nil, fmt.Errorf("invalid version '%s': %s", metaVersionRange, versionResult.Error) + } + minVersion = versionResult.Version + maxVersion = versionResult.Version + } + + return minVersion, maxVersion, nil +} + +type Versions struct { + // raw contains the original version string from the meta file. + raw string + // ranges contains the list of version ranges where the class/property is supported. + ranges []VersionRange +} + +func (v *Versions) Raw() string { + // Raw returns the original version string from the meta file. + return v.raw +} + +func (v *Versions) Ranges() []VersionRange { + // Ranges returns the list of version ranges. + return v.ranges +} + +func (v *Versions) String() string { + // String returns the version ranges as a formatted string for documentation. + var versionRanges []string + for _, versionRange := range v.ranges { + versionRanges = append(versionRanges, versionRange.String()) + } + return strings.Join(versionRanges, ", ") +} + +func (v *Versions) Sort() { + // Sort orders the version ranges from lowest to highest. + // Ranges are compared first by their minimum version, then by their maximum version. + // nil values are treated as unbounded: nil min comes first, nil max comes last. + slices.SortFunc(v.ranges, sortVersionRanges) +} + +func sortVersionRanges(a, b VersionRange) int { + // sortVersionRanges compares two VersionRange structs for sorting. + // Returns -1 if a < b, 0 if a == b, 1 if a > b. + + // Compare by minimum version first + minCmp := sortVersions(a.min, b.min, true) + if minCmp != 0 { + return minCmp + } + + // If min versions are equal, compare by max version + return sortVersions(a.max, b.max, false) +} + +func sortVersions(a, b *provider.Version, isMinBound bool) int { + // sortVersions compares two version pointers for sorting. + // isMinBound determines how nil values are ordered: + // - true (min bound): nil sorts first, representing "no lower bound" (earliest) + // - false (max bound): nil sorts last, representing "no upper bound" (latest) + aIsNil := a == nil + bIsNil := b == nil + + switch { + case aIsNil && bIsNil: + // Unlikely in practice, but needed for correctness if both bounds are unbounded. + return 0 + case aIsNil && isMinBound: + return -1 + case aIsNil: + return 1 + case bIsNil && isMinBound: + return 1 + case bIsNil: + return -1 + case provider.IsVersionEqual(*a, *b): + return 0 + case provider.IsVersionLesser(*a, *b): + return -1 + default: + return 1 + } +} + +func NewVersions(metaVersions string) (*Versions, error) { + // NewVersions parses a raw version string from meta files and returns a Versions struct. + // The version string can contain multiple comma-separated ranges. + // Each range can be: + // - A single version: "1.0(1e)" (min and max are the same) + // - A bounded range: "4.2(7f)-4.2(7w)" (min to max) + // - An unbounded range: "4.2(7f)-" (min to unlimited, max is nil) + // + // Example: "3.2(10e)-3.2(10g),3.2(7f)-" produces two ranges. + genLogger.Trace(fmt.Sprintf("Parsing version string: %s", metaVersions)) + + ranges, err := parseVersionRanges(metaVersions) + if err != nil { + return nil, err + } + + versions := &Versions{raw: metaVersions, ranges: ranges} + versions.Sort() + + genLogger.Trace(fmt.Sprintf("Successfully parsed version string '%s' into %d ranges.", metaVersions, len(versions.ranges))) + genLogger.Trace(fmt.Sprintf("Constructed version string: %s", versions)) + return versions, nil +} + +func parseVersionRanges(metaVersions string) ([]VersionRange, error) { + // parseVersionRanges parses a comma-separated list of version ranges. + // Example: "4.2(7f)-4.2(7w),5.2(1g)-" -> [VersionRange{4.2(7f), 4.2(7w)}, VersionRange{5.2(1g), nil}] + var versionRanges []VersionRange + + for metaVersionRange := range strings.SplitSeq(metaVersions, ",") { + versionRange, err := NewVersionRange(metaVersionRange) + if err != nil { + return nil, err + } + versionRanges = append(versionRanges, *versionRange) + } + + return versionRanges, nil +} diff --git a/gen/utils/data/version_test.go b/gen/utils/data/version_test.go new file mode 100644 index 000000000..f239c1ad2 --- /dev/null +++ b/gen/utils/data/version_test.go @@ -0,0 +1,631 @@ +package data + +import ( + "testing" + + "github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils/test" + "github.com/CiscoDevNet/terraform-provider-aci/v2/internal/provider" + "github.com/stretchr/testify/assert" +) + +type newVersionRangeExpected struct { + Raw string + Min *provider.Version + Max *provider.Version + IsSingleVersion bool + String string + Error bool + ErrorMsg string +} + +func TestNewVersionRange(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + testCases := []test.TestCase{ + { + Name: "test_single_version_with_tag", + Input: "4.2(7f)", + Expected: newVersionRangeExpected{ + Raw: "4.2(7f)", + Min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + Max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsSingleVersion: true, + String: "4.2(7f)", + }, + }, + { + Name: "test_single_version_without_tag", + Input: "4.2(7)", + Expected: newVersionRangeExpected{ + Raw: "4.2(7)", + Min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: 0}, + Max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: 0}, + IsSingleVersion: true, + String: "4.2(7)", + }, + }, + { + Name: "test_bounded_range", + Input: "4.2(7f)-4.2(7w)", + Expected: newVersionRangeExpected{ + Raw: "4.2(7f)-4.2(7w)", + Min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + Max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + IsSingleVersion: false, + String: "4.2(7f) to 4.2(7w)", + }, + }, + { + Name: "test_unbounded_upper", + Input: "4.2(7f)-", + Expected: newVersionRangeExpected{ + Raw: "4.2(7f)-", + Min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + Max: nil, + IsSingleVersion: false, + String: "4.2(7f) and later", + }, + }, + { + Name: "test_unbounded_lower", + Input: "-4.2(7w)", + Expected: newVersionRangeExpected{ + Raw: "-4.2(7w)", + Min: nil, + Max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + IsSingleVersion: false, + String: "up to 4.2(7w)", + }, + }, + { + Name: "test_error_invalid_version", + Input: "invalid", + Expected: newVersionRangeExpected{ + Error: true, + ErrorMsg: "invalid version 'invalid': unknown", + }, + }, + { + Name: "test_error_invalid_min_version", + Input: "invalid-4.2(7w)", + Expected: newVersionRangeExpected{ + Error: true, + ErrorMsg: "invalid minimum version 'invalid': unknown", + }, + }, + { + Name: "test_error_invalid_max_version", + Input: "4.2(7f)-invalid", + Expected: newVersionRangeExpected{ + Error: true, + ErrorMsg: "invalid maximum version 'invalid': unknown", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + expected := testCase.Expected.(newVersionRangeExpected) + versionRange, err := NewVersionRange(testCase.Input.(string)) + + if expected.Error { + assert.EqualError(t, err, expected.ErrorMsg) + } else { + assert.NoError(t, err, test.MessageUnexpectedError(err)) + assert.Equal(t, expected.Raw, versionRange.Raw(), test.MessageEqual(expected.Raw, versionRange.Raw(), testCase.Name)) + assert.Equal(t, expected.Min, versionRange.Min(), test.MessageEqual(expected.Min, versionRange.Min(), testCase.Name)) + assert.Equal(t, expected.Max, versionRange.Max(), test.MessageEqual(expected.Max, versionRange.Max(), testCase.Name)) + assert.Equal(t, expected.IsSingleVersion, versionRange.IsSingleVersion(), test.MessageEqual(expected.IsSingleVersion, versionRange.IsSingleVersion(), testCase.Name)) + assert.Equal(t, expected.String, versionRange.String(), test.MessageEqual(expected.String, versionRange.String(), testCase.Name)) + } + }) + } +} + +type newVersionsExpected struct { + Raw string + RangeCount int + String string + RangeValues []newVersionRangeExpected + Error bool + ErrorMsg string +} + +func TestNewVersions(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + testCases := []test.TestCase{ + { + Name: "test_single_version", + Input: "4.2(7f)", + Expected: newVersionsExpected{ + Raw: "4.2(7f)", + RangeCount: 1, + String: "4.2(7f)", + RangeValues: []newVersionRangeExpected{ + { + Raw: "4.2(7f)", + Min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + Max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsSingleVersion: true, + String: "4.2(7f)", + }, + }, + }, + }, + { + Name: "test_unbounded_range", + Input: "4.2(7f)-", + Expected: newVersionsExpected{ + Raw: "4.2(7f)-", + RangeCount: 1, + String: "4.2(7f) and later", + RangeValues: []newVersionRangeExpected{ + { + Raw: "4.2(7f)-", + Min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + Max: nil, + IsSingleVersion: false, + String: "4.2(7f) and later", + }, + }, + }, + }, + { + Name: "test_multiple_ranges", + Input: "3.2(10e)-3.2(10g),4.2(7f)-", + Expected: newVersionsExpected{ + Raw: "3.2(10e)-3.2(10g),4.2(7f)-", + RangeCount: 2, + String: "3.2(10e) to 3.2(10g), 4.2(7f) and later", + RangeValues: []newVersionRangeExpected{ + { + Raw: "3.2(10e)-3.2(10g)", + Min: &provider.Version{Major: 3, Minor: 2, Patch: 10, Tag: int('e')}, + Max: &provider.Version{Major: 3, Minor: 2, Patch: 10, Tag: int('g')}, + IsSingleVersion: false, + String: "3.2(10e) to 3.2(10g)", + }, + { + Raw: "4.2(7f)-", + Min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + Max: nil, + IsSingleVersion: false, + String: "4.2(7f) and later", + }, + }, + }, + }, + { + Name: "test_multiple_ranges_sorted", + Input: "5.2(1g)-,3.2(10e)-3.2(10g)", + Expected: newVersionsExpected{ + Raw: "5.2(1g)-,3.2(10e)-3.2(10g)", + RangeCount: 2, + String: "3.2(10e) to 3.2(10g), 5.2(1g) and later", + RangeValues: []newVersionRangeExpected{ + { + Raw: "3.2(10e)-3.2(10g)", + Min: &provider.Version{Major: 3, Minor: 2, Patch: 10, Tag: int('e')}, + Max: &provider.Version{Major: 3, Minor: 2, Patch: 10, Tag: int('g')}, + IsSingleVersion: false, + String: "3.2(10e) to 3.2(10g)", + }, + { + Raw: "5.2(1g)-", + Min: &provider.Version{Major: 5, Minor: 2, Patch: 1, Tag: int('g')}, + Max: nil, + IsSingleVersion: false, + String: "5.2(1g) and later", + }, + }, + }, + }, + { + Name: "test_three_ranges_sorted", + Input: "5.0(1a)-,3.0(1a)-3.0(1z),4.0(1a)-4.0(1z)", + Expected: newVersionsExpected{ + Raw: "5.0(1a)-,3.0(1a)-3.0(1z),4.0(1a)-4.0(1z)", + RangeCount: 3, + String: "3.0(1a) to 3.0(1z), 4.0(1a) to 4.0(1z), 5.0(1a) and later", + RangeValues: []newVersionRangeExpected{ + { + Raw: "3.0(1a)-3.0(1z)", + Min: &provider.Version{Major: 3, Minor: 0, Patch: 1, Tag: int('a')}, + Max: &provider.Version{Major: 3, Minor: 0, Patch: 1, Tag: int('z')}, + IsSingleVersion: false, + String: "3.0(1a) to 3.0(1z)", + }, + { + Raw: "4.0(1a)-4.0(1z)", + Min: &provider.Version{Major: 4, Minor: 0, Patch: 1, Tag: int('a')}, + Max: &provider.Version{Major: 4, Minor: 0, Patch: 1, Tag: int('z')}, + IsSingleVersion: false, + String: "4.0(1a) to 4.0(1z)", + }, + { + Raw: "5.0(1a)-", + Min: &provider.Version{Major: 5, Minor: 0, Patch: 1, Tag: int('a')}, + Max: nil, + IsSingleVersion: false, + String: "5.0(1a) and later", + }, + }, + }, + }, + { + Name: "test_error_invalid_version", + Input: "invalid", + Expected: newVersionsExpected{ + Error: true, + ErrorMsg: "invalid version 'invalid': unknown", + }, + }, + { + Name: "test_error_invalid_version_in_multiple", + Input: "4.2(7f)-,invalid", + Expected: newVersionsExpected{ + Error: true, + ErrorMsg: "invalid version 'invalid': unknown", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + expected := testCase.Expected.(newVersionsExpected) + versions, err := NewVersions(testCase.Input.(string)) + + if expected.Error { + assert.EqualError(t, err, expected.ErrorMsg) + } else { + assert.NoError(t, err, test.MessageUnexpectedError(err)) + assert.Equal(t, expected.Raw, versions.Raw(), test.MessageEqual(expected.Raw, versions.Raw(), testCase.Name)) + assert.Equal(t, expected.RangeCount, len(versions.Ranges()), test.MessageEqual(expected.RangeCount, len(versions.Ranges()), testCase.Name)) + assert.Equal(t, expected.String, versions.String(), test.MessageEqual(expected.String, versions.String(), testCase.Name)) + + // Verify each range + for i, expectedRange := range expected.RangeValues { + actualRange := versions.Ranges()[i] + assert.Equal(t, expectedRange.Raw, actualRange.Raw(), test.MessageEqual(expectedRange.Raw, actualRange.Raw(), testCase.Name)) + assert.Equal(t, expectedRange.Min, actualRange.Min(), test.MessageEqual(expectedRange.Min, actualRange.Min(), testCase.Name)) + assert.Equal(t, expectedRange.Max, actualRange.Max(), test.MessageEqual(expectedRange.Max, actualRange.Max(), testCase.Name)) + assert.Equal(t, expectedRange.IsSingleVersion, actualRange.IsSingleVersion(), test.MessageEqual(expectedRange.IsSingleVersion, actualRange.IsSingleVersion(), testCase.Name)) + assert.Equal(t, expectedRange.String, actualRange.String(), test.MessageEqual(expectedRange.String, actualRange.String(), testCase.Name)) + } + } + }) + } +} + +type sortVersionsExpected struct { + Result int +} + +type sortVersionsInput struct { + A *provider.Version + B *provider.Version + IsMinBound bool +} + +func TestSortVersions(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + testCases := []test.TestCase{ + { + Name: "test_both_nil_min_bound", + Input: sortVersionsInput{ + A: nil, + B: nil, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: 0}, + }, + { + Name: "test_both_nil_max_bound", + Input: sortVersionsInput{ + A: nil, + B: nil, + IsMinBound: false, + }, + Expected: sortVersionsExpected{Result: 0}, + }, + { + Name: "test_a_nil_min_bound", + Input: sortVersionsInput{ + A: nil, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: -1}, + }, + { + Name: "test_a_nil_max_bound", + Input: sortVersionsInput{ + A: nil, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: false, + }, + Expected: sortVersionsExpected{Result: 1}, + }, + { + Name: "test_b_nil_min_bound", + Input: sortVersionsInput{ + A: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + B: nil, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: 1}, + }, + { + Name: "test_b_nil_max_bound", + Input: sortVersionsInput{ + A: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + B: nil, + IsMinBound: false, + }, + Expected: sortVersionsExpected{Result: -1}, + }, + { + Name: "test_equal_versions", + Input: sortVersionsInput{ + A: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: 0}, + }, + { + Name: "test_a_less_than_b_major", + Input: sortVersionsInput{ + A: &provider.Version{Major: 3, Minor: 2, Patch: 7, Tag: int('f')}, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: -1}, + }, + { + Name: "test_a_greater_than_b_major", + Input: sortVersionsInput{ + A: &provider.Version{Major: 5, Minor: 2, Patch: 7, Tag: int('f')}, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: 1}, + }, + { + Name: "test_a_less_than_b_minor", + Input: sortVersionsInput{ + A: &provider.Version{Major: 4, Minor: 1, Patch: 7, Tag: int('f')}, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: -1}, + }, + { + Name: "test_a_less_than_b_patch", + Input: sortVersionsInput{ + A: &provider.Version{Major: 4, Minor: 2, Patch: 6, Tag: int('f')}, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: -1}, + }, + { + Name: "test_a_less_than_b_tag", + Input: sortVersionsInput{ + A: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('e')}, + B: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + IsMinBound: true, + }, + Expected: sortVersionsExpected{Result: -1}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + input := testCase.Input.(sortVersionsInput) + expected := testCase.Expected.(sortVersionsExpected) + result := sortVersions(input.A, input.B, input.IsMinBound) + + assert.Equal(t, expected.Result, result, test.MessageEqual(expected.Result, result, testCase.Name)) + }) + } +} + +type sortVersionRangesExpected struct { + Result int +} + +type sortVersionRangesInput struct { + A VersionRange + B VersionRange +} + +func TestSortVersionRanges(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + testCases := []test.TestCase{ + { + Name: "test_equal_ranges", + Input: sortVersionRangesInput{ + A: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + B: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + }, + Expected: sortVersionRangesExpected{Result: 0}, + }, + { + Name: "test_a_min_less_than_b_min", + Input: sortVersionRangesInput{ + A: VersionRange{ + min: &provider.Version{Major: 3, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 3, Minor: 2, Patch: 7, Tag: int('w')}, + }, + B: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + }, + Expected: sortVersionRangesExpected{Result: -1}, + }, + { + Name: "test_same_min_a_max_less_than_b_max", + Input: sortVersionRangesInput{ + A: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('g')}, + }, + B: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + }, + Expected: sortVersionRangesExpected{Result: -1}, + }, + { + Name: "test_same_min_a_unbounded_max", + Input: sortVersionRangesInput{ + A: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: nil, + }, + B: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + }, + Expected: sortVersionRangesExpected{Result: 1}, + }, + { + Name: "test_a_unbounded_min", + Input: sortVersionRangesInput{ + A: VersionRange{ + min: nil, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + B: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + }, + Expected: sortVersionRangesExpected{Result: -1}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + input := testCase.Input.(sortVersionRangesInput) + expected := testCase.Expected.(sortVersionRangesExpected) + result := sortVersionRanges(input.A, input.B) + + assert.Equal(t, expected.Result, result, test.MessageEqual(expected.Result, result, testCase.Name)) + }) + } +} + +func TestVersionsSort(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + // Create unsorted versions + versions := &Versions{ + raw: "5.0(1a)-,3.0(1a)-3.0(1z),4.0(1a)-4.0(1z)", + ranges: []VersionRange{ + { + raw: "5.0(1a)-", + min: &provider.Version{Major: 5, Minor: 0, Patch: 1, Tag: int('a')}, + max: nil, + }, + { + raw: "3.0(1a)-3.0(1z)", + min: &provider.Version{Major: 3, Minor: 0, Patch: 1, Tag: int('a')}, + max: &provider.Version{Major: 3, Minor: 0, Patch: 1, Tag: int('z')}, + }, + { + raw: "4.0(1a)-4.0(1z)", + min: &provider.Version{Major: 4, Minor: 0, Patch: 1, Tag: int('a')}, + max: &provider.Version{Major: 4, Minor: 0, Patch: 1, Tag: int('z')}, + }, + }, + } + + // Sort + versions.Sort() + + // Verify order + assert.Equal(t, 3, versions.Ranges()[0].Min().Major, "First range should have Major=3") + assert.Equal(t, 4, versions.Ranges()[1].Min().Major, "Second range should have Major=4") + assert.Equal(t, 5, versions.Ranges()[2].Min().Major, "Third range should have Major=5") +} + +func TestVersionRangeString(t *testing.T) { + t.Parallel() + test.InitializeTest(t) + + testCases := []test.TestCase{ + { + Name: "test_single_version", + Input: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + }, + Expected: "4.2(7f)", + }, + { + Name: "test_single_version_no_tag", + Input: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: 0}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: 0}, + }, + Expected: "4.2(7)", + }, + { + Name: "test_bounded_range", + Input: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + Expected: "4.2(7f) to 4.2(7w)", + }, + { + Name: "test_unbounded_upper", + Input: VersionRange{ + min: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('f')}, + max: nil, + }, + Expected: "4.2(7f) and later", + }, + { + Name: "test_unbounded_lower", + Input: VersionRange{ + min: nil, + max: &provider.Version{Major: 4, Minor: 2, Patch: 7, Tag: int('w')}, + }, + Expected: "up to 4.2(7w)", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + versionRange := testCase.Input.(VersionRange) + expected := testCase.Expected.(string) + + assert.Equal(t, expected, versionRange.String(), test.MessageEqual(expected, versionRange.String(), testCase.Name)) + }) + } +} diff --git a/gen/utils/logger/logger_test.go b/gen/utils/logger/logger_test.go index de6512350..5532b8e2a 100644 --- a/gen/utils/logger/logger_test.go +++ b/gen/utils/logger/logger_test.go @@ -8,7 +8,6 @@ import ( "github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils/test" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -18,7 +17,7 @@ const ( func initializeLogTest(t *testing.T) *Logger { test.InitializeTest(t) genLogger := InitializeLogger() - require.NotNil(t, genLogger, "logger must be initialized") + assert.NotNil(t, genLogger, "logger must be initialized") return genLogger } @@ -38,7 +37,7 @@ func TestLogFile(t *testing.T) { genLogger := initializeLogTest(t) - require.NotNil(t, genLogger.logFile) + assert.NotNil(t, genLogger.logFile) assert.Equal(t, logFilePath, genLogger.logFile.Name(), test.MessageEqual(logFilePath, genLogger.logFile.Name(), t.Name())) } @@ -86,7 +85,7 @@ func TestSetLogLevel(t *testing.T) { } bytes, err := os.ReadFile(logFilePath) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) logFileContent := string(bytes) for _, testCase := range testCases { diff --git a/gen/utils/test/helpers.go b/gen/utils/test/helpers.go index 693294fda..70abe79bd 100644 --- a/gen/utils/test/helpers.go +++ b/gen/utils/test/helpers.go @@ -32,7 +32,7 @@ func MessageNotContains(collection, element any, caseName string) string { return fmt.Sprintf("Expected '%v' to not contain '%v' for case '%s'", collection, element, caseName) } -// MessageNotEmpty returns a formatted message for require.NotEmpty comparisons. +// MessageNotEmpty returns a formatted message for assert.NotEmpty comparisons. func MessageNotEmpty(object any, caseName string) string { return fmt.Sprintf("Expected '%v' to not be empty for case '%s'", object, caseName) } diff --git a/gen/utils/utils_test.go b/gen/utils/utils_test.go index 88af4687c..83a7f608a 100644 --- a/gen/utils/utils_test.go +++ b/gen/utils/utils_test.go @@ -5,7 +5,6 @@ import ( "github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils/test" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -24,7 +23,7 @@ func TestGetFileNamesFromDirectoryWithExtension(t *testing.T) { filenames := GetFileNamesFromDirectory(constTestDirectoryForGetFileNamesFromDirectory, false) - require.NotEmpty(t, filenames, test.MessageNotEmpty(filenames, t.Name())) + assert.NotEmpty(t, filenames, test.MessageNotEmpty(filenames, t.Name())) assert.Len(t, filenames, 3) assert.Contains(t, filenames, constTestFile1WithExtension, test.MessageContains(filenames, constTestFile1WithExtension, t.Name())) assert.Contains(t, filenames, constTestFile2WithExtension, test.MessageContains(filenames, constTestFile2WithExtension, t.Name())) @@ -38,7 +37,7 @@ func TestGetFileNamesFromDirectoryWithoutExtension(t *testing.T) { filenames := GetFileNamesFromDirectory(constTestDirectoryForGetFileNamesFromDirectory, true) - require.NotEmpty(t, filenames, test.MessageNotEmpty(filenames, t.Name())) + assert.NotEmpty(t, filenames, test.MessageNotEmpty(filenames, t.Name())) assert.Len(t, filenames, 3) assert.Contains(t, filenames, constTestFile1WithoutExtension, test.MessageContains(filenames, constTestFile1WithoutExtension, t.Name())) assert.Contains(t, filenames, constTestFile2WithoutExtension, test.MessageContains(filenames, constTestFile2WithoutExtension, t.Name())) @@ -82,7 +81,7 @@ func TestPlural(t *testing.T) { t.Run(testCase.Name, func(t *testing.T) { t.Parallel() result, err := Plural(testCase.Input.(string)) - require.NoError(t, err, test.MessageUnexpectedError(err)) + assert.NoError(t, err, test.MessageUnexpectedError(err)) assert.Equal(t, testCase.Expected, result, test.MessageEqual(testCase.Expected, result, testCase.Name)) }) } @@ -93,7 +92,7 @@ func TestPluralError(t *testing.T) { test.InitializeTest(t) _, err := Plural("contracts") - require.Error(t, err) + assert.Error(t, err) assert.ErrorContains(t, err, "no plural rule defined") } diff --git a/internal/provider/function_compare_versions.go b/internal/provider/function_compare_versions.go index 942457e00..fab598bab 100644 --- a/internal/provider/function_compare_versions.go +++ b/internal/provider/function_compare_versions.go @@ -172,13 +172,20 @@ type Version struct { Tag int } +func (v Version) String() string { + if v.Tag != 0 { + return fmt.Sprintf("%d.%d(%d%c)", v.Major, v.Minor, v.Patch, v.Tag) + } + return fmt.Sprintf("%d.%d(%d)", v.Major, v.Minor, v.Patch) +} + type VersionResult struct { Version *Version Error string } func ParseVersion(rawVersion string) VersionResult { - versionRegex := regexp.MustCompile(`(\d+)\.(\d+)\((\d+)([a-z])\)`) + versionRegex := regexp.MustCompile(`(\d+)\.(\d+)\((\d+)([a-z])?\)`) matches := versionRegex.FindStringSubmatch(rawVersion) if matches == nil { return VersionResult{Error: "unknown"} @@ -186,7 +193,10 @@ func ParseVersion(rawVersion string) VersionResult { major, _ := strconv.Atoi(matches[1]) minor, _ := strconv.Atoi(matches[2]) patch, _ := strconv.Atoi(matches[3]) - tag := int(matches[4][0]) + var tag int + if matches[4] != "" { + tag = int(matches[4][0]) + } return VersionResult{Version: &Version{Major: major, Minor: minor, Patch: patch, Tag: tag}}