stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
// <copyright file="ModelCreationInfoTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ModelCreationInfoTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedCreatedAt = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void Spdx3CreationInfo_IsValidSpecVersion_Works()
|
||||
{
|
||||
var valid = new Spdx3CreationInfo
|
||||
{
|
||||
SpecVersion = "3.0.1",
|
||||
Created = FixedCreatedAt
|
||||
};
|
||||
|
||||
var invalid = new Spdx3CreationInfo
|
||||
{
|
||||
SpecVersion = "2.3",
|
||||
Created = FixedCreatedAt
|
||||
};
|
||||
|
||||
Assert.True(valid.IsValidSpecVersion());
|
||||
Assert.False(invalid.IsValidSpecVersion());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// <copyright file="ModelDocumentTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ModelDocumentTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spdx3Document_ConformsTo_Works()
|
||||
{
|
||||
var packages = new[] { new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "test" } };
|
||||
var profiles = new[] { Spdx3ProfileIdentifier.Software, Spdx3ProfileIdentifier.Core };
|
||||
var doc = new Spdx3Document(packages, [], profiles);
|
||||
|
||||
Assert.True(doc.ConformsTo(Spdx3ProfileIdentifier.Software));
|
||||
Assert.True(doc.ConformsTo(Spdx3ProfileIdentifier.Core));
|
||||
Assert.False(doc.ConformsTo(Spdx3ProfileIdentifier.Build));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Document_GetRootPackage_Works()
|
||||
{
|
||||
var pkg1 = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "root" };
|
||||
var pkg2 = new Spdx3Package { SpdxId = "urn:test:pkg2", Name = "dep" };
|
||||
var relationship = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.Contains
|
||||
};
|
||||
|
||||
var doc = new Spdx3Document(
|
||||
[pkg1, pkg2, relationship],
|
||||
[],
|
||||
[Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
var root = doc.GetRootPackage();
|
||||
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal("root", root.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// <copyright file="ModelExternalIdentifierTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ModelExternalIdentifierTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExternalIdentifierExtensions_GetPurl_Works()
|
||||
{
|
||||
var identifiers = new[]
|
||||
{
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "pkg:npm/lodash@4.17.21"
|
||||
},
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.Cpe23,
|
||||
Identifier = "cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:*:*:*"
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal("pkg:npm/lodash@4.17.21", identifiers.GetPurl());
|
||||
Assert.Equal("cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:*:*:*", identifiers.GetCpe23());
|
||||
}
|
||||
}
|
||||
110
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ModelHashTests.cs
Normal file
110
src/__Libraries/__Tests/StellaOps.Spdx3.Tests/ModelHashTests.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
// <copyright file="ModelHashTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 hash helpers.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ModelHashTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spdx3Hash_NormalizesValue()
|
||||
{
|
||||
var hash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"
|
||||
};
|
||||
|
||||
Assert.Equal(
|
||||
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
hash.NormalizedHashValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_ValidatesHex()
|
||||
{
|
||||
var validHash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
};
|
||||
|
||||
var invalidHash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "xyz-not-hex!"
|
||||
};
|
||||
|
||||
Assert.True(validHash.IsValidHex());
|
||||
Assert.False(invalidHash.IsValidHex());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_ValidatesLength()
|
||||
{
|
||||
var validSha256 = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
};
|
||||
|
||||
var invalidSha256 = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef"
|
||||
};
|
||||
|
||||
Assert.True(validSha256.IsValidLength());
|
||||
Assert.False(invalidSha256.IsValidLength());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha512, 128)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha3_256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Blake2b256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, 32)]
|
||||
public void Spdx3Hash_GetExpectedLength_ReturnsCorrectLength(
|
||||
Spdx3HashAlgorithm algorithm,
|
||||
int expected)
|
||||
{
|
||||
var hash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = algorithm,
|
||||
HashValue = new string('a', expected)
|
||||
};
|
||||
|
||||
Assert.Equal(expected, hash.GetExpectedLength());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha512, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Blake2b256, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, false)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha1, false)]
|
||||
public void HashAlgorithm_IsRecommended_ReturnsCorrectValue(
|
||||
Spdx3HashAlgorithm algorithm,
|
||||
bool expected)
|
||||
{
|
||||
Assert.Equal(expected, algorithm.IsRecommended());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha1, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, false)]
|
||||
public void HashAlgorithm_IsDeprecated_ReturnsCorrectValue(
|
||||
Spdx3HashAlgorithm algorithm,
|
||||
bool expected)
|
||||
{
|
||||
Assert.Equal(expected, algorithm.IsDeprecated());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// <copyright file="ModelPackageTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 package model behavior.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ModelPackageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spdx3Package_Equality_Works()
|
||||
{
|
||||
var pkg1 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "test-package",
|
||||
PackageVersion = "1.0.0"
|
||||
};
|
||||
|
||||
var pkg2 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "test-package",
|
||||
PackageVersion = "1.0.0"
|
||||
};
|
||||
|
||||
var pkg3 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg2",
|
||||
Name = "other-package",
|
||||
PackageVersion = "2.0.0"
|
||||
};
|
||||
|
||||
Assert.Equal(pkg1, pkg2);
|
||||
Assert.NotEqual(pkg1, pkg3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// <copyright file="ModelProfileUriTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ModelProfileUriTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_ParseUri_Works()
|
||||
{
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Software));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Core, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Core));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Build, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Build));
|
||||
Assert.Null(Spdx3ProfileUris.ParseUri("https://unknown.example.com"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_Parse_WorksWithNames()
|
||||
{
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.Parse("Software"));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.Parse("software"));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Build, Spdx3ProfileUris.Parse("BUILD"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_GetUri_Works()
|
||||
{
|
||||
Assert.Equal(Spdx3ProfileUris.Software, Spdx3ProfileUris.GetUri(Spdx3ProfileIdentifier.Software));
|
||||
Assert.Equal(Spdx3ProfileUris.Core, Spdx3ProfileUris.GetUri(Spdx3ProfileIdentifier.Core));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// <copyright file="ModelRelationshipTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 relationship model behavior.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ModelRelationshipTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spdx3Relationship_TypeMapping_Works()
|
||||
{
|
||||
var relationship = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
|
||||
Assert.Equal(Spdx3RelationshipType.DependsOn, relationship.RelationshipType);
|
||||
Assert.Single(relationship.To);
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
// <copyright file="ModelTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 model classes.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spdx3Package_Equality_Works()
|
||||
{
|
||||
// Arrange
|
||||
var pkg1 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "test-package",
|
||||
PackageVersion = "1.0.0"
|
||||
};
|
||||
|
||||
var pkg2 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "test-package",
|
||||
PackageVersion = "1.0.0"
|
||||
};
|
||||
|
||||
var pkg3 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg2",
|
||||
Name = "other-package",
|
||||
PackageVersion = "2.0.0"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(pkg1, pkg2);
|
||||
Assert.NotEqual(pkg1, pkg3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Relationship_TypeMapping_Works()
|
||||
{
|
||||
// Arrange
|
||||
var relationship = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3RelationshipType.DependsOn, relationship.RelationshipType);
|
||||
Assert.Single(relationship.To);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_NormalizesValue()
|
||||
{
|
||||
// Arrange
|
||||
var hash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
hash.NormalizedHashValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_ValidatesHex()
|
||||
{
|
||||
// Arrange
|
||||
var validHash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||
};
|
||||
|
||||
var invalidHash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "xyz-not-hex!"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(validHash.IsValidHex());
|
||||
Assert.False(invalidHash.IsValidHex());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Hash_ValidatesLength()
|
||||
{
|
||||
// Arrange
|
||||
var validSha256 = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" // 64 chars
|
||||
};
|
||||
|
||||
var invalidSha256 = new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "abcdef" // too short
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(validSha256.IsValidLength());
|
||||
Assert.False(invalidSha256.IsValidLength());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha512, 128)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha3_256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Blake2b256, 64)]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, 32)]
|
||||
public void Spdx3Hash_GetExpectedLength_ReturnsCorrectLength(Spdx3HashAlgorithm algorithm, int expected)
|
||||
{
|
||||
// Arrange
|
||||
var hash = new Spdx3Hash
|
||||
{
|
||||
Algorithm = algorithm,
|
||||
HashValue = new string('a', expected)
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, hash.GetExpectedLength());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha512, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Blake2b256, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, false)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha1, false)]
|
||||
public void HashAlgorithm_IsRecommended_ReturnsCorrectValue(Spdx3HashAlgorithm algorithm, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, algorithm.IsRecommended());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3HashAlgorithm.Md5, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha1, true)]
|
||||
[InlineData(Spdx3HashAlgorithm.Sha256, false)]
|
||||
public void HashAlgorithm_IsDeprecated_ReturnsCorrectValue(Spdx3HashAlgorithm algorithm, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, algorithm.IsDeprecated());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_ParseUri_Works()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Software));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Core, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Core));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Build, Spdx3ProfileUris.ParseUri(Spdx3ProfileUris.Build));
|
||||
Assert.Null(Spdx3ProfileUris.ParseUri("https://unknown.example.com"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_Parse_WorksWithNames()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.Parse("Software"));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Software, Spdx3ProfileUris.Parse("software"));
|
||||
Assert.Equal(Spdx3ProfileIdentifier.Build, Spdx3ProfileUris.Parse("BUILD"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3ProfileIdentifier_GetUri_Works()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(Spdx3ProfileUris.Software, Spdx3ProfileUris.GetUri(Spdx3ProfileIdentifier.Software));
|
||||
Assert.Equal(Spdx3ProfileUris.Core, Spdx3ProfileUris.GetUri(Spdx3ProfileIdentifier.Core));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalIdentifierExtensions_GetPurl_Works()
|
||||
{
|
||||
// Arrange
|
||||
var identifiers = new[]
|
||||
{
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "pkg:npm/lodash@4.17.21"
|
||||
},
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.Cpe23,
|
||||
Identifier = "cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:*:*:*"
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal("pkg:npm/lodash@4.17.21", identifiers.GetPurl());
|
||||
Assert.Equal("cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:*:*:*", identifiers.GetCpe23());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3CreationInfo_IsValidSpecVersion_Works()
|
||||
{
|
||||
// Arrange
|
||||
var valid = new Spdx3CreationInfo
|
||||
{
|
||||
SpecVersion = "3.0.1",
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var invalid = new Spdx3CreationInfo
|
||||
{
|
||||
SpecVersion = "2.3",
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(valid.IsValidSpecVersion());
|
||||
Assert.False(invalid.IsValidSpecVersion());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Document_ConformsTo_Works()
|
||||
{
|
||||
// Arrange
|
||||
var packages = new[] { new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "test" } };
|
||||
var profiles = new[] { Spdx3ProfileIdentifier.Software, Spdx3ProfileIdentifier.Core };
|
||||
var doc = new Spdx3Document(packages, [], profiles);
|
||||
|
||||
// Assert
|
||||
Assert.True(doc.ConformsTo(Spdx3ProfileIdentifier.Software));
|
||||
Assert.True(doc.ConformsTo(Spdx3ProfileIdentifier.Core));
|
||||
Assert.False(doc.ConformsTo(Spdx3ProfileIdentifier.Build));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spdx3Document_GetRootPackage_Works()
|
||||
{
|
||||
// Arrange
|
||||
var pkg1 = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "root" };
|
||||
var pkg2 = new Spdx3Package { SpdxId = "urn:test:pkg2", Name = "dep" };
|
||||
var relationship = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.Contains
|
||||
};
|
||||
|
||||
var doc = new Spdx3Document(
|
||||
[pkg1, pkg2, relationship],
|
||||
[],
|
||||
[Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var root = doc.GetRootPackage();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal("root", root.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// <copyright file="ParserDocumentLookupTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ParserDocumentLookupTests : IClassFixture<Spdx3ParserFixture>
|
||||
{
|
||||
private readonly Spdx3Parser _parser;
|
||||
|
||||
public ParserDocumentLookupTests(Spdx3ParserFixture fixture)
|
||||
{
|
||||
_parser = fixture.Parser;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_DocumentGetById_ReturnsElementAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var pkg = result.Document.GetById<Spdx3Package>("urn:spdx:example:package-1");
|
||||
Assert.NotNull(pkg);
|
||||
Assert.Equal("example-app", pkg.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_DocumentGetDependencies_ReturnsDepsAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var deps = result.Document.GetDependencies("urn:spdx:example:package-1").ToList();
|
||||
Assert.Single(deps);
|
||||
Assert.Equal("lodash", deps[0].Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// <copyright file="ParserExtractionTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ParserExtractionTests : IClassFixture<Spdx3ParserFixture>
|
||||
{
|
||||
private readonly Spdx3Parser _parser;
|
||||
|
||||
public ParserExtractionTests(Spdx3ParserFixture fixture)
|
||||
{
|
||||
_parser = fixture.Parser;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_ExtractsPackagesAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var packages = result.Document.Packages;
|
||||
Assert.Equal(2, packages.Length);
|
||||
|
||||
var mainPackage = packages.FirstOrDefault(p => p.Name == "example-app");
|
||||
Assert.NotNull(mainPackage);
|
||||
Assert.Equal("1.0.0", mainPackage.PackageVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_ExtractsRelationshipsAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var relationships = result.Document.Relationships;
|
||||
Assert.Equal(2, relationships.Length);
|
||||
|
||||
var dependsOn = relationships.FirstOrDefault(r => r.RelationshipType == Spdx3RelationshipType.DependsOn);
|
||||
Assert.NotNull(dependsOn);
|
||||
Assert.Equal("urn:spdx:example:package-1", dependsOn.From);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_ExtractsCreationInfoAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Software, result.Document.Profiles);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Core, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_ExtractsPurlAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var purls = result.Document.GetAllPurls().ToList();
|
||||
Assert.Contains("pkg:npm/example-app@1.0.0", purls);
|
||||
Assert.Contains("pkg:npm/lodash@4.17.21", purls);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// <copyright file="ParserFailureTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ParserFailureTests : IClassFixture<Spdx3ParserFixture>
|
||||
{
|
||||
private readonly Spdx3Parser _parser;
|
||||
|
||||
public ParserFailureTests(Spdx3ParserFixture fixture)
|
||||
{
|
||||
_parser = fixture.Parser;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_InvalidNoContext_ReturnsFailAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "invalid-no-context.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains(result.Errors, e => e.Code == "MISSING_CONTEXT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_NonexistentFile_ReturnsFailAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync("nonexistent-file.json", ct);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains(result.Errors, e => e.Code == "FILE_NOT_FOUND");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// <copyright file="ParserJsonTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ParserJsonTests : IClassFixture<Spdx3ParserFixture>
|
||||
{
|
||||
private readonly Spdx3Parser _parser;
|
||||
|
||||
public ParserJsonTests(Spdx3ParserFixture fixture)
|
||||
{
|
||||
_parser = fixture.Parser;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseFromJson_ValidJson_ParsesAsync()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:test:pkg1",
|
||||
"name": "test-package",
|
||||
"packageVersion": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseFromJsonAsync(json, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Single(result.Document.Packages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// <copyright file="ParserProfileSamplesTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class ParserProfileSamplesTests : IClassFixture<Spdx3ParserFixture>
|
||||
{
|
||||
private readonly Spdx3Parser _parser;
|
||||
|
||||
public ParserProfileSamplesTests(Spdx3ParserFixture fixture)
|
||||
{
|
||||
_parser = fixture.Parser;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_ValidSoftwareProfile_ReturnsSuccessAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success, string.Join(", ", result.Errors.Select(e => e.Message)));
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.True(result.Document.Packages.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_ValidLiteProfile_ReturnsSuccessAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-lite-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Lite, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_ValidBuildProfile_ReturnsSuccessAsync()
|
||||
{
|
||||
var samplePath = Path.Combine("Samples", "valid-build-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Build, result.Document.Profiles);
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
// <copyright file="ParserTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Spdx3.JsonLd;
|
||||
using StellaOps.Spdx3.Model;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 parser.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ParserTests : IDisposable
|
||||
{
|
||||
private readonly Spdx3Parser _parser;
|
||||
private readonly MemoryCache _cache;
|
||||
|
||||
public ParserTests()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
var options = Options.Create(new Spdx3ContextResolverOptions { AllowRemoteContexts = false });
|
||||
var resolver = new Spdx3ContextResolver(
|
||||
httpClientFactory.Object,
|
||||
_cache,
|
||||
NullLogger<Spdx3ContextResolver>.Instance,
|
||||
options,
|
||||
TimeProvider.System);
|
||||
|
||||
_parser = new Spdx3Parser(resolver, NullLogger<Spdx3Parser>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ValidSoftwareProfile_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success, string.Join(", ", result.Errors.Select(e => e.Message)));
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.True(result.Document.Packages.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ValidLiteProfile_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-lite-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Lite, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ValidBuildProfile_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-build-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Build, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_InvalidNoContext_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "invalid-no-context.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains(result.Errors, e => e.Code == "MISSING_CONTEXT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsPackages()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var packages = result.Document.Packages;
|
||||
Assert.Equal(2, packages.Length);
|
||||
|
||||
var mainPackage = packages.FirstOrDefault(p => p.Name == "example-app");
|
||||
Assert.NotNull(mainPackage);
|
||||
Assert.Equal("1.0.0", mainPackage.PackageVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsRelationships()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var relationships = result.Document.Relationships;
|
||||
Assert.Equal(2, relationships.Length);
|
||||
|
||||
var dependsOn = relationships.FirstOrDefault(r => r.RelationshipType == Spdx3RelationshipType.DependsOn);
|
||||
Assert.NotNull(dependsOn);
|
||||
Assert.Equal("urn:spdx:example:package-1", dependsOn.From);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsCreationInfo()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Software, result.Document.Profiles);
|
||||
Assert.Contains(Spdx3ProfileIdentifier.Core, result.Document.Profiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsPurl()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var purls = result.Document.GetAllPurls().ToList();
|
||||
Assert.Contains("pkg:npm/example-app@1.0.0", purls);
|
||||
Assert.Contains("pkg:npm/lodash@4.17.21", purls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_NonexistentFile_ReturnsFail()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync("nonexistent-file.json", ct);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains(result.Errors, e => e.Code == "FILE_NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseFromJsonAsync_ValidJson_Parses()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "software_Package",
|
||||
"spdxId": "urn:test:pkg1",
|
||||
"name": "test-package",
|
||||
"packageVersion": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseFromJsonAsync(json, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
Assert.Single(result.Document.Packages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_DocumentGetById_ReturnsElement()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var pkg = result.Document.GetById<Model.Software.Spdx3Package>("urn:spdx:example:package-1");
|
||||
Assert.NotNull(pkg);
|
||||
Assert.Equal("example-app", pkg.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_DocumentGetDependencies_ReturnsDeps()
|
||||
{
|
||||
// Arrange
|
||||
var samplePath = Path.Combine("Samples", "valid-software-profile.json");
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(samplePath, ct);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Document);
|
||||
|
||||
var deps = result.Document.GetDependencies("urn:spdx:example:package-1").ToList();
|
||||
Assert.Single(deps);
|
||||
Assert.Equal("lodash", deps[0].Name);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// <copyright file="Spdx3ParserBenchmarks.MemoryTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
public sealed partial class Spdx3ParserBenchmarks
|
||||
{
|
||||
[Fact]
|
||||
public async Task Benchmark_MemoryUsage_StaysWithinBoundsAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = CreateDocument(1000);
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var memBefore = GC.GetTotalMemory(true);
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var memAfter = GC.GetTotalMemory(true);
|
||||
var memDelta = memAfter - memBefore;
|
||||
|
||||
var maxMemGrowthBytes = 50 * 1024 * 1024L;
|
||||
|
||||
Assert.True(
|
||||
memDelta < maxMemGrowthBytes,
|
||||
$"Memory grew by {memDelta / 1024.0 / 1024.0:F2}MB, target < {maxMemGrowthBytes / 1024.0 / 1024.0:F2}MB");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"Memory growth after 10 parses: {memDelta / 1024.0 / 1024.0:F2}MB");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// <copyright file="Spdx3ParserBenchmarks.ScalingTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
public sealed partial class Spdx3ParserBenchmarks
|
||||
{
|
||||
[Fact]
|
||||
public async Task Benchmark_ScalingCharacteristics_SubLinearAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var sizes = new[] { 100, 500, 1000, 2000 };
|
||||
var timings = new Dictionary<int, double>();
|
||||
|
||||
foreach (var size in sizes)
|
||||
{
|
||||
var documentPath = CreateDocument(size);
|
||||
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
timings[size] = sw.Elapsed.TotalMilliseconds / 5;
|
||||
}
|
||||
|
||||
var time100 = timings[100];
|
||||
var time1000 = timings[1000];
|
||||
var scalingFactor = time1000 / time100;
|
||||
var expectedScaling = 10.0;
|
||||
var maxScaling = expectedScaling * 2.5;
|
||||
|
||||
Assert.True(
|
||||
scalingFactor < maxScaling,
|
||||
$"Scaling factor {scalingFactor:F2}x exceeds target {maxScaling:F2}x (expected ~{expectedScaling:F2}x)");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine("Scaling results:");
|
||||
foreach (var (size, time) in timings)
|
||||
{
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($" {size} elements: {time:F2}ms");
|
||||
}
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($"Scaling factor (100 -> 1000): {scalingFactor:F2}x");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// <copyright file="Spdx3ParserBenchmarks.SizeTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
public sealed partial class Spdx3ParserBenchmarks
|
||||
{
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse100Elements_CompletesWithinTargetAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = CreateDocument(100);
|
||||
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 100.0;
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"100-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"100-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse1000Elements_CompletesWithinTargetAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = CreateDocument(1000);
|
||||
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 500.0;
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"1000-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"1000-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse10000Elements_CompletesWithinTargetAsync()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = CreateDocument(10000);
|
||||
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 5000.0;
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"10000-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"10000-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
// <copyright file="Spdx3ParserBenchmarks.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Spdx3.JsonLd;
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
@@ -18,20 +15,16 @@ namespace StellaOps.Spdx3.Tests;
|
||||
/// Task: SP3-018 - Validate parsing performance for various document sizes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These tests measure parsing performance and compare against 2.x parser baseline.
|
||||
/// Target: SPDX 3.0.1 parser should be within 2x of 2.x parser performance.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To run as proper benchmarks, consider using BenchmarkDotNet in a dedicated
|
||||
/// benchmark project (StellaOps.Spdx3.Benchmarks).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Trait("Category", "Performance")]
|
||||
public sealed class Spdx3ParserBenchmarks : IDisposable
|
||||
[Trait("Intent", "Performance")]
|
||||
public sealed partial class Spdx3ParserBenchmarks : IDisposable
|
||||
{
|
||||
private const int WarmupIterations = 3;
|
||||
private const int BenchmarkIterations = 10;
|
||||
private static readonly string TempDirRoot = Path.Combine(Path.GetTempPath(), "stellaops-tests", "spdx3-benchmarks");
|
||||
|
||||
private readonly Spdx3Parser _parser;
|
||||
private readonly MemoryCache _cache;
|
||||
@@ -50,297 +43,26 @@ public sealed class Spdx3ParserBenchmarks : IDisposable
|
||||
TimeProvider.System);
|
||||
|
||||
_parser = new Spdx3Parser(resolver, NullLogger<Spdx3Parser>.Instance);
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), $"spdx3-bench-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse100Elements_CompletesWithinTarget()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(100);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 100.0; // Target: < 100ms for 100 elements
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"100-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
// Log for visibility
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"100-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse1000Elements_CompletesWithinTarget()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(1000);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 500.0; // Target: < 500ms for 1000 elements
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"1000-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"1000-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_Parse10000Elements_CompletesWithinTarget()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(10000);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
|
||||
// Act
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success, $"Parse failed: {string.Join(", ", result.Errors.Select(e => e.Message))}");
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.Elapsed.TotalMilliseconds / BenchmarkIterations;
|
||||
var maxTargetMs = 5000.0; // Target: < 5000ms for 10000 elements
|
||||
|
||||
Assert.True(
|
||||
avgMs < maxTargetMs,
|
||||
$"10000-element parse averaged {avgMs:F2}ms, target < {maxTargetMs}ms");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"10000-element parse: {avgMs:F2}ms average over {BenchmarkIterations} iterations");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_ScalingCharacteristics_SubLinear()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var sizes = new[] { 100, 500, 1000, 2000 };
|
||||
var timings = new Dictionary<int, double>();
|
||||
|
||||
foreach (var size in sizes)
|
||||
{
|
||||
var documentPath = GenerateSpdx3Document(size);
|
||||
|
||||
// Warmup
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
|
||||
// Measure
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
await _parser.ParseAsync(documentPath, ct);
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
timings[size] = sw.Elapsed.TotalMilliseconds / 5;
|
||||
}
|
||||
|
||||
// Assert: scaling should be roughly linear (within 2x expected)
|
||||
// If 100 elements takes T, then 1000 should take ~10T (not 100T)
|
||||
var time100 = timings[100];
|
||||
var time1000 = timings[1000];
|
||||
var scalingFactor = time1000 / time100;
|
||||
var expectedScaling = 10.0; // Linear scaling
|
||||
var maxScaling = expectedScaling * 2.5; // Allow 2.5x tolerance
|
||||
|
||||
Assert.True(
|
||||
scalingFactor < maxScaling,
|
||||
$"Scaling factor {scalingFactor:F2}x exceeds target {maxScaling:F2}x (expected ~{expectedScaling:F2}x)");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($"Scaling results:");
|
||||
foreach (var (size, time) in timings)
|
||||
{
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($" {size} elements: {time:F2}ms");
|
||||
}
|
||||
TestContext.Current.TestOutputHelper?.WriteLine($"Scaling factor (100 -> 1000): {scalingFactor:F2}x");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Benchmark_MemoryUsage_StaysWithinBounds()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var documentPath = GenerateSpdx3Document(1000);
|
||||
|
||||
// Force GC before measurement
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var memBefore = GC.GetTotalMemory(true);
|
||||
|
||||
// Act
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var result = await _parser.ParseAsync(documentPath, ct);
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
|
||||
// Allow time for finalization
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
var memAfter = GC.GetTotalMemory(true);
|
||||
var memDelta = memAfter - memBefore;
|
||||
|
||||
// Assert: memory growth should be bounded (not leaking)
|
||||
// Allow up to 50MB growth for 10 parses of 1000-element docs
|
||||
var maxMemGrowthBytes = 50 * 1024 * 1024L;
|
||||
|
||||
Assert.True(
|
||||
memDelta < maxMemGrowthBytes,
|
||||
$"Memory grew by {memDelta / 1024.0 / 1024.0:F2}MB, target < {maxMemGrowthBytes / 1024.0 / 1024.0:F2}MB");
|
||||
|
||||
TestContext.Current.TestOutputHelper?.WriteLine(
|
||||
$"Memory growth after 10 parses: {memDelta / 1024.0 / 1024.0:F2}MB");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an SPDX 3.0.1 JSON-LD document with the specified number of package elements.
|
||||
/// </summary>
|
||||
private string GenerateSpdx3Document(int packageCount)
|
||||
{
|
||||
var graph = new JsonArray();
|
||||
|
||||
// Add SpdxDocument root
|
||||
var document = new JsonObject
|
||||
{
|
||||
["@type"] = "SpdxDocument",
|
||||
["@id"] = "https://stellaops.org/spdx/benchmark-doc",
|
||||
["spdxId"] = "https://stellaops.org/spdx/benchmark-doc",
|
||||
["name"] = $"Benchmark Document ({packageCount} packages)",
|
||||
["specVersion"] = "3.0.1",
|
||||
["creationInfo"] = new JsonObject
|
||||
{
|
||||
["@type"] = "CreationInfo",
|
||||
["created"] = "2026-01-08T00:00:00Z",
|
||||
["createdBy"] = new JsonArray { "https://stellaops.org/spdx/tool/benchmark" },
|
||||
["specVersion"] = "3.0.1"
|
||||
},
|
||||
["rootElement"] = new JsonArray { "https://stellaops.org/spdx/benchmark-root-pkg" },
|
||||
["profileConformance"] = new JsonArray { "core", "software" }
|
||||
};
|
||||
graph.Add(document);
|
||||
|
||||
// Add root package
|
||||
var rootPackage = new JsonObject
|
||||
{
|
||||
["@type"] = "software_Package",
|
||||
["@id"] = "https://stellaops.org/spdx/benchmark-root-pkg",
|
||||
["spdxId"] = "https://stellaops.org/spdx/benchmark-root-pkg",
|
||||
["name"] = "benchmark-root",
|
||||
["packageVersion"] = "1.0.0",
|
||||
["downloadLocation"] = "https://example.com/benchmark-root-1.0.0.tar.gz"
|
||||
};
|
||||
graph.Add(rootPackage);
|
||||
|
||||
// Add package elements
|
||||
for (var i = 0; i < packageCount; i++)
|
||||
{
|
||||
var pkg = new JsonObject
|
||||
{
|
||||
["@type"] = "software_Package",
|
||||
["@id"] = $"https://stellaops.org/spdx/pkg-{i:D5}",
|
||||
["spdxId"] = $"https://stellaops.org/spdx/pkg-{i:D5}",
|
||||
["name"] = $"package-{i:D5}",
|
||||
["packageVersion"] = $"{i / 100}.{i % 100}.0",
|
||||
["downloadLocation"] = $"https://example.com/pkg-{i:D5}.tar.gz",
|
||||
["externalIdentifier"] = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["@type"] = "ExternalIdentifier",
|
||||
["externalIdentifierType"] = "packageUrl",
|
||||
["identifier"] = $"pkg:generic/package-{i:D5}@{i / 100}.{i % 100}.0"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add some relationships to make it more realistic
|
||||
if (i > 0 && i % 10 == 0)
|
||||
{
|
||||
var relationship = new JsonObject
|
||||
{
|
||||
["@type"] = "Relationship",
|
||||
["@id"] = $"https://stellaops.org/spdx/rel-{i:D5}",
|
||||
["spdxId"] = $"https://stellaops.org/spdx/rel-{i:D5}",
|
||||
["relationshipType"] = "dependsOn",
|
||||
["from"] = $"https://stellaops.org/spdx/pkg-{i:D5}",
|
||||
["to"] = new JsonArray { $"https://stellaops.org/spdx/pkg-{i - 1:D5}" }
|
||||
};
|
||||
graph.Add(relationship);
|
||||
}
|
||||
|
||||
graph.Add(pkg);
|
||||
}
|
||||
|
||||
var root = new JsonObject
|
||||
{
|
||||
["@context"] = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
["@graph"] = graph
|
||||
};
|
||||
|
||||
var filePath = Path.Combine(_tempDir, $"spdx3-{packageCount}-elements.json");
|
||||
var json = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = false });
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
return filePath;
|
||||
_tempDir = TempDirRoot;
|
||||
ResetTempDir();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
TryDeleteTempDir();
|
||||
}
|
||||
|
||||
// Clean up temp files
|
||||
private string CreateDocument(int packageCount) => Spdx3BenchmarkDocumentBuilder.WriteDocument(_tempDir, packageCount);
|
||||
|
||||
private void ResetTempDir()
|
||||
{
|
||||
TryDeleteTempDir();
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
private void TryDeleteTempDir()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempDir))
|
||||
@@ -350,7 +72,7 @@ public sealed class Spdx3ParserBenchmarks : IDisposable
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
// Best effort cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,3 +9,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0038-T | DONE | Revalidated 2026-01-08; open findings tracked in audit report. |
|
||||
| AUDIT-0038-A | DONE | Waived (test project; revalidated 2026-01-08). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-03 | DONE | Tier 0 remediation (usings normalized); dotnet test passed 2026-02-02 (63 tests, MTP0001 warning). |
|
||||
| REMED-04 | DONE | Async naming applied; ConfigureAwait(false) skipped in tests per xUnit1030; dotnet test passed 2026-02-02. |
|
||||
| REMED-05 | DONE | Parser/validator/model/benchmarks split <= 100 lines; deterministic benchmark paths and fixed timestamps; dotnet test passed 2026-02-02. |
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// <copyright file="Spdx3BenchmarkDocumentBuilder.Packages.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
internal static partial class Spdx3BenchmarkDocumentBuilder
|
||||
{
|
||||
private static JsonObject CreateRootPackage()
|
||||
{
|
||||
return new JsonObject
|
||||
{
|
||||
["@type"] = "software_Package",
|
||||
["@id"] = RootPackageId,
|
||||
["spdxId"] = RootPackageId,
|
||||
["name"] = "benchmark-root",
|
||||
["packageVersion"] = "1.0.0",
|
||||
["downloadLocation"] = "https://example.com/benchmark-root-1.0.0.tar.gz"
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject CreatePackage(int index)
|
||||
{
|
||||
var id = FormattableString.Invariant($"https://stellaops.org/spdx/pkg-{index:D5}");
|
||||
var version = FormattableString.Invariant($"{index / 100}.{index % 100}.0");
|
||||
return new JsonObject
|
||||
{
|
||||
["@type"] = "software_Package",
|
||||
["@id"] = id,
|
||||
["spdxId"] = id,
|
||||
["name"] = FormattableString.Invariant($"package-{index:D5}"),
|
||||
["packageVersion"] = version,
|
||||
["downloadLocation"] = FormattableString.Invariant($"https://example.com/pkg-{index:D5}.tar.gz"),
|
||||
["externalIdentifier"] = new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
["@type"] = "ExternalIdentifier",
|
||||
["externalIdentifierType"] = "packageUrl",
|
||||
["identifier"] = FormattableString.Invariant($"pkg:generic/package-{index:D5}@{version}")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject CreateRelationship(int index)
|
||||
{
|
||||
return new JsonObject
|
||||
{
|
||||
["@type"] = "Relationship",
|
||||
["@id"] = FormattableString.Invariant($"https://stellaops.org/spdx/rel-{index:D5}"),
|
||||
["spdxId"] = FormattableString.Invariant($"https://stellaops.org/spdx/rel-{index:D5}"),
|
||||
["relationshipType"] = "dependsOn",
|
||||
["from"] = FormattableString.Invariant($"https://stellaops.org/spdx/pkg-{index:D5}"),
|
||||
["to"] = new JsonArray { FormattableString.Invariant($"https://stellaops.org/spdx/pkg-{index - 1:D5}") }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// <copyright file="Spdx3BenchmarkDocumentBuilder.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
internal static partial class Spdx3BenchmarkDocumentBuilder
|
||||
{
|
||||
private const string ContextUri = "https://spdx.org/rdf/3.0.1/spdx-context.jsonld";
|
||||
private const string DocumentId = "https://stellaops.org/spdx/benchmark-doc";
|
||||
private const string RootPackageId = "https://stellaops.org/spdx/benchmark-root-pkg";
|
||||
|
||||
public static string WriteDocument(string directory, int packageCount)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
var graph = new JsonArray { CreateDocument(packageCount), CreateRootPackage() };
|
||||
for (var i = 0; i < packageCount; i++)
|
||||
{
|
||||
graph.Add(CreatePackage(i));
|
||||
if (i > 0 && i % 10 == 0)
|
||||
{
|
||||
graph.Add(CreateRelationship(i));
|
||||
}
|
||||
}
|
||||
|
||||
var root = new JsonObject
|
||||
{
|
||||
["@context"] = ContextUri,
|
||||
["@graph"] = graph
|
||||
};
|
||||
|
||||
var filePath = Path.Combine(directory, FormattableString.Invariant($"spdx3-{packageCount}-elements.json"));
|
||||
var json = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = false });
|
||||
File.WriteAllText(filePath, json);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private static JsonObject CreateDocument(int packageCount)
|
||||
{
|
||||
return new JsonObject
|
||||
{
|
||||
["@type"] = "SpdxDocument",
|
||||
["@id"] = DocumentId,
|
||||
["spdxId"] = DocumentId,
|
||||
["name"] = FormattableString.Invariant($"Benchmark Document ({packageCount} packages)"),
|
||||
["specVersion"] = "3.0.1",
|
||||
["creationInfo"] = new JsonObject
|
||||
{
|
||||
["@type"] = "CreationInfo",
|
||||
["created"] = "2026-01-08T00:00:00Z",
|
||||
["createdBy"] = new JsonArray { "https://stellaops.org/spdx/tool/benchmark" },
|
||||
["specVersion"] = "3.0.1"
|
||||
},
|
||||
["rootElement"] = new JsonArray { RootPackageId },
|
||||
["profileConformance"] = new JsonArray { "core", "software" }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// <copyright file="Spdx3ParserFixture.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Spdx3.JsonLd;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
public sealed class Spdx3ParserFixture : IDisposable
|
||||
{
|
||||
private readonly MemoryCache _cache;
|
||||
|
||||
public Spdx3ParserFixture()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
var options = Options.Create(new Spdx3ContextResolverOptions { AllowRemoteContexts = false });
|
||||
var resolver = new Spdx3ContextResolver(
|
||||
httpClientFactory.Object,
|
||||
_cache,
|
||||
NullLogger<Spdx3ContextResolver>.Instance,
|
||||
options,
|
||||
TimeProvider.System);
|
||||
|
||||
Parser = new Spdx3Parser(resolver, NullLogger<Spdx3Parser>.Instance);
|
||||
}
|
||||
|
||||
public Spdx3Parser Parser { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// <copyright file="Spdx3ValidatorFixtures.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
|
||||
internal static class Spdx3ValidatorFixtures
|
||||
{
|
||||
public static Spdx3Document CreateValidDocument()
|
||||
{
|
||||
var pkg1 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "root-package",
|
||||
PackageVersion = "1.0.0",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b9344ca495991b7852b855"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var pkg2 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg2",
|
||||
Name = "dep-package",
|
||||
PackageVersion = "2.0.0",
|
||||
ExternalIdentifier =
|
||||
[
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "pkg:npm/dep-package@2.0.0"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
|
||||
return new Spdx3Document(
|
||||
[pkg1, pkg2, rel],
|
||||
[],
|
||||
[Spdx3ProfileIdentifier.Software, Spdx3ProfileIdentifier.Core]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// <copyright file="ValidatorBasicTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
using StellaOps.Spdx3.Validation;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Safety")]
|
||||
public sealed class ValidatorBasicTests
|
||||
{
|
||||
private readonly Spdx3Validator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidDocument_ReturnsValid()
|
||||
{
|
||||
var doc = Spdx3ValidatorFixtures.CreateValidDocument();
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyDocument_ReturnsError()
|
||||
{
|
||||
var doc = new Spdx3Document([], [], []);
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "EMPTY_DOCUMENT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DuplicateSpdxId_ReturnsError()
|
||||
{
|
||||
var pkg1 = new Spdx3Package { SpdxId = "urn:test:dup", Name = "pkg1" };
|
||||
var pkg2 = new Spdx3Package { SpdxId = "urn:test:dup", Name = "pkg2" };
|
||||
var doc = new Spdx3Document([pkg1, pkg2], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "DUPLICATE_SPDX_ID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Info_ContainsStats()
|
||||
{
|
||||
var doc = Spdx3ValidatorFixtures.CreateValidDocument();
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.Contains(result.Info, i => i.Code == "DOCUMENT_STATS");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// <copyright file="ValidatorExternalIdentifierTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Validation;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Safety")]
|
||||
public sealed class ValidatorExternalIdentifierTests
|
||||
{
|
||||
private readonly Spdx3Validator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_InvalidPurl_ReturnsWarning()
|
||||
{
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
ExternalIdentifier =
|
||||
[
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "not-a-valid-purl"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.Contains(result.Warnings, w => w.Code == "INVALID_PURL_FORMAT");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// <copyright file="ValidatorHashTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Validation;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Safety")]
|
||||
public sealed class ValidatorHashTests
|
||||
{
|
||||
private readonly Spdx3Validator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_InvalidHashFormat_ReturnsError()
|
||||
{
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "not-hex-value!"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "INVALID_HASH_FORMAT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DeprecatedHashAlgorithm_ReturnsWarning()
|
||||
{
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Md5,
|
||||
HashValue = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.Contains(result.Warnings, w => w.Code == "DEPRECATED_HASH_ALGORITHM");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// <copyright file="ValidatorOptionsTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Tests.TestInfrastructure;
|
||||
using StellaOps.Spdx3.Validation;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Safety")]
|
||||
public sealed class ValidatorOptionsTests
|
||||
{
|
||||
private readonly Spdx3Validator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_RequiredProfileMissing_ReturnsError()
|
||||
{
|
||||
var doc = Spdx3ValidatorFixtures.CreateValidDocument();
|
||||
var options = new Spdx3ValidationOptions
|
||||
{
|
||||
RequiredProfiles = [Spdx3ProfileIdentifier.Build]
|
||||
};
|
||||
|
||||
var result = _validator.Validate(doc, options);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "MISSING_REQUIRED_PROFILE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_TreatWarningsAsErrors_ConvertsWarnings()
|
||||
{
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Md5,
|
||||
HashValue = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
var options = new Spdx3ValidationOptions { TreatWarningsAsErrors = true };
|
||||
|
||||
var result = _validator.Validate(doc, options);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "DEPRECATED_HASH_ALGORITHM");
|
||||
Assert.Empty(result.Warnings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// <copyright file="ValidatorRelationshipTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Validation;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Safety")]
|
||||
public sealed class ValidatorRelationshipTests
|
||||
{
|
||||
private readonly Spdx3Validator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_DanglingRelationship_ReturnsWarning()
|
||||
{
|
||||
var pkg = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "pkg1" };
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:nonexistent"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
var doc = new Spdx3Document([pkg, rel], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Contains(result.Warnings, w => w.Code == "DANGLING_RELATIONSHIP_TO");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyRelationshipTo_ReturnsError()
|
||||
{
|
||||
var pkg = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "pkg1" };
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = [],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
var doc = new Spdx3Document([pkg, rel], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "EMPTY_RELATIONSHIP_TO");
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
// <copyright file="ValidatorTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
using StellaOps.Spdx3.Validation;
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX 3.0.1 validator.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ValidatorTests
|
||||
{
|
||||
private readonly Spdx3Validator _validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidDocument_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
var doc = CreateValidDocument();
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Empty(result.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyDocument_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var doc = new Spdx3Document([], [], []);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "EMPTY_DOCUMENT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DuplicateSpdxId_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pkg1 = new Spdx3Package { SpdxId = "urn:test:dup", Name = "pkg1" };
|
||||
var pkg2 = new Spdx3Package { SpdxId = "urn:test:dup", Name = "pkg2" };
|
||||
var doc = new Spdx3Document([pkg1, pkg2], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "DUPLICATE_SPDX_ID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DanglingRelationship_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "pkg1" };
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:nonexistent"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
var doc = new Spdx3Document([pkg, rel], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid); // Warnings don't fail validation by default
|
||||
Assert.Contains(result.Warnings, w => w.Code == "DANGLING_RELATIONSHIP_TO");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyRelationshipTo_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package { SpdxId = "urn:test:pkg1", Name = "pkg1" };
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = [],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
var doc = new Spdx3Document([pkg, rel], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "EMPTY_RELATIONSHIP_TO");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_InvalidPurl_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
ExternalIdentifier =
|
||||
[
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "not-a-valid-purl"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Warnings, w => w.Code == "INVALID_PURL_FORMAT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_InvalidHashFormat_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "not-hex-value!"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "INVALID_HASH_FORMAT");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DeprecatedHashAlgorithm_ReturnsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Md5,
|
||||
HashValue = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Warnings, w => w.Code == "DEPRECATED_HASH_ALGORITHM");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_RequiredProfileMissing_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
var doc = CreateValidDocument();
|
||||
var options = new Spdx3ValidationOptions
|
||||
{
|
||||
RequiredProfiles = [Spdx3ProfileIdentifier.Build]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc, options);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "MISSING_REQUIRED_PROFILE");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_TreatWarningsAsErrors_ConvertsWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var pkg = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "pkg1",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Md5,
|
||||
HashValue = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
}
|
||||
]
|
||||
};
|
||||
var doc = new Spdx3Document([pkg], [], [Spdx3ProfileIdentifier.Software]);
|
||||
var options = new Spdx3ValidationOptions { TreatWarningsAsErrors = true };
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc, options);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.Code == "DEPRECATED_HASH_ALGORITHM");
|
||||
Assert.Empty(result.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Info_ContainsStats()
|
||||
{
|
||||
// Arrange
|
||||
var doc = CreateValidDocument();
|
||||
|
||||
// Act
|
||||
var result = _validator.Validate(doc);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(result.Info, i => i.Code == "DOCUMENT_STATS");
|
||||
}
|
||||
|
||||
private static Spdx3Document CreateValidDocument()
|
||||
{
|
||||
var pkg1 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg1",
|
||||
Name = "root-package",
|
||||
PackageVersion = "1.0.0",
|
||||
VerifiedUsing =
|
||||
[
|
||||
new Spdx3Hash
|
||||
{
|
||||
Algorithm = Spdx3HashAlgorithm.Sha256,
|
||||
HashValue = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var pkg2 = new Spdx3Package
|
||||
{
|
||||
SpdxId = "urn:test:pkg2",
|
||||
Name = "dep-package",
|
||||
PackageVersion = "2.0.0",
|
||||
ExternalIdentifier =
|
||||
[
|
||||
new Spdx3ExternalIdentifier
|
||||
{
|
||||
ExternalIdentifierType = Spdx3ExternalIdentifierType.PackageUrl,
|
||||
Identifier = "pkg:npm/dep-package@2.0.0"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var rel = new Spdx3Relationship
|
||||
{
|
||||
SpdxId = "urn:test:rel1",
|
||||
From = "urn:test:pkg1",
|
||||
To = ["urn:test:pkg2"],
|
||||
RelationshipType = Spdx3RelationshipType.DependsOn
|
||||
};
|
||||
|
||||
return new Spdx3Document(
|
||||
[pkg1, pkg2, rel],
|
||||
[],
|
||||
[Spdx3ProfileIdentifier.Software, Spdx3ProfileIdentifier.Core]);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
// <copyright file="VersionDetectorTests.cs" company="StellaOps">
|
||||
// <copyright file="VersionDetectorDetectionTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for SPDX version detection.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class VersionDetectorTests
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class VersionDetectorDetectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Detect_Spdx301JsonLd_ReturnsCorrectVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
||||
@@ -21,10 +17,8 @@ public sealed class VersionDetectorTests
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx301, result.Version);
|
||||
Assert.True(result.IsJsonLd);
|
||||
}
|
||||
@@ -32,7 +26,6 @@ public sealed class VersionDetectorTests
|
||||
[Fact]
|
||||
public void Detect_Spdx23_ReturnsCorrectVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
@@ -41,10 +34,8 @@ public sealed class VersionDetectorTests
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx23, result.Version);
|
||||
Assert.False(result.IsJsonLd);
|
||||
Assert.Equal("SPDX-2.3", result.VersionString);
|
||||
@@ -53,7 +44,6 @@ public sealed class VersionDetectorTests
|
||||
[Fact]
|
||||
public void Detect_Spdx22_ReturnsCorrectVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
@@ -62,10 +52,8 @@ public sealed class VersionDetectorTests
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx22, result.Version);
|
||||
Assert.False(result.IsJsonLd);
|
||||
}
|
||||
@@ -73,7 +61,6 @@ public sealed class VersionDetectorTests
|
||||
[Fact]
|
||||
public void Detect_Unknown_ReturnsUnknown()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"random": "data",
|
||||
@@ -81,17 +68,14 @@ public sealed class VersionDetectorTests
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Unknown, result.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Detect_ContextWithArray_DetectsVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": [
|
||||
@@ -102,10 +86,8 @@ public sealed class VersionDetectorTests
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx301, result.Version);
|
||||
Assert.True(result.IsJsonLd);
|
||||
}
|
||||
@@ -113,7 +95,6 @@ public sealed class VersionDetectorTests
|
||||
[Fact]
|
||||
public void Detect_SpecVersionInGraph_DetectsVersion()
|
||||
{
|
||||
// Arrange
|
||||
var json = """
|
||||
{
|
||||
"@context": "https://example.com/context",
|
||||
@@ -127,26 +108,8 @@ public sealed class VersionDetectorTests
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = Spdx3VersionDetector.Detect(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Spdx3VersionDetector.SpdxVersion.Spdx301, result.Version);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx22, "Use SpdxParser (SPDX 2.x parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx23, "Use SpdxParser (SPDX 2.x parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx301, "Use Spdx3Parser (SPDX 3.0.1 parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Unknown, "Unknown format - manual inspection required")]
|
||||
public void GetParserRecommendation_ReturnsCorrectRecommendation(
|
||||
Spdx3VersionDetector.SpdxVersion version,
|
||||
string expected)
|
||||
{
|
||||
// Act
|
||||
var recommendation = Spdx3VersionDetector.GetParserRecommendation(version);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, recommendation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// <copyright file="VersionDetectorRecommendationTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
// </copyright>
|
||||
namespace StellaOps.Spdx3.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
[Trait("Intent", "Operational")]
|
||||
public sealed class VersionDetectorRecommendationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx22, "Use SpdxParser (SPDX 2.x parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx23, "Use SpdxParser (SPDX 2.x parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Spdx301, "Use Spdx3Parser (SPDX 3.0.1 parser)")]
|
||||
[InlineData(Spdx3VersionDetector.SpdxVersion.Unknown, "Unknown format - manual inspection required")]
|
||||
public void GetParserRecommendation_ReturnsCorrectRecommendation(
|
||||
Spdx3VersionDetector.SpdxVersion version,
|
||||
string expected)
|
||||
{
|
||||
var recommendation = Spdx3VersionDetector.GetParserRecommendation(version);
|
||||
|
||||
Assert.Equal(expected, recommendation);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user