up
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"entrypoint": "bin/ed.js;cli.js;dist/feature.browser.js;dist/feature.node.js;dist/main.js;dist/module.mjs",
|
||||
"entrypoint.conditions": "browser;import;node;require",
|
||||
"entrypoint.conditions": "./feature,browser;./feature,node",
|
||||
"path": "."
|
||||
},
|
||||
"evidence": [
|
||||
@@ -34,13 +34,13 @@
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/feature.browser.js;browser"
|
||||
"value": "dist/feature.browser.js;./feature,browser"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/feature.node.js;node"
|
||||
"value": "dist/feature.node.js;./feature,node"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
@@ -48,24 +48,12 @@
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/main.js;dist/main.js"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/main.js;require"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/module.mjs;dist/module.mjs"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/module.mjs;import"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -19,14 +19,14 @@
|
||||
"source": "node-version:dockerfile",
|
||||
"locator": "Dockerfile",
|
||||
"value": "18.17.1-alpine",
|
||||
"sha256": "b38d145059ea1b7018105f769070f1d07276b30719ce20358f673bef9655bcdf"
|
||||
"sha256": "209fa7a3a7b852f71bb272ba1a4b062a97cefb9cc98e5596150e198e430b1917"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "node-version:nvmrc",
|
||||
"locator": ".nvmrc",
|
||||
"value": "18.17.1",
|
||||
"sha256": "cbc986933feddabb31649808506d635bb5d74667ba2da9aafc46ffe706ec745b"
|
||||
"sha256": "80c39ad40c34cb6c53bf9d02100eb9766b7a3d3c1d0572d7ce3a89f8fc0fd106"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
@@ -34,34 +34,5 @@
|
||||
"locator": "package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/tar-demo@1.2.3",
|
||||
"purl": "pkg:npm/tar-demo@1.2.3",
|
||||
"name": "tar-demo",
|
||||
"version": "1.2.3",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"installScripts": "true",
|
||||
"path": "tgz",
|
||||
"policyHint.installLifecycle": "install",
|
||||
"script.install": "echo install"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "tgz/tar-demo.tgz!package/package.json",
|
||||
"sha256": "dd27b49de19040a8b5738d4ad0d17ef2041e5ac8a6c5300dbace9be8fcf3ed67"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:scripts",
|
||||
"locator": "tgz/tar-demo.tgz!package/package.json#scripts.install",
|
||||
"value": "echo install"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -46,4 +46,10 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Exclude OpenSsl shared files since they're already included via Lang.Tests reference -->
|
||||
<ItemGroup>
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslLegacyShim.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslAutoInit.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
104
src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/Fixtures/lang/php/container/composer.lock
generated
Normal file
104
src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/Fixtures/lang/php/container/composer.lock
generated
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"content-hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"plugin-api-version": "2.6.0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "slim/slim",
|
||||
"version": "4.12.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/slimphp/Slim.git",
|
||||
"reference": "d1e2f3a4d1e2f3a4d1e2f3a4d1e2f3a4d1e2f3a4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Slim\\": "Slim"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "2.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "e2f3a4b5e2f3a4b5e2f3a4b5e2f3a4b5e2f3a4b5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "predis/predis",
|
||||
"version": "2.2.2",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/predis/predis.git",
|
||||
"reference": "f3a4b5c6f3a4b5c6f3a4b5c6f3a4b5c6f3a4b5c6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Predis\\": "src/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ext-redis",
|
||||
"version": "6.0.2",
|
||||
"type": "php-ext",
|
||||
"dist": {
|
||||
"type": "pecl",
|
||||
"url": "https://pecl.php.net/get/redis-6.0.2.tgz"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mongodb/mongodb",
|
||||
"version": "1.17.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mongodb/mongo-php-library.git",
|
||||
"reference": "a4b5c6d7a4b5c6d7a4b5c6d7a4b5c6d7a4b5c6d7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MongoDB\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ext-mongodb",
|
||||
"version": "1.17.0",
|
||||
"type": "php-ext",
|
||||
"dist": {
|
||||
"type": "pecl",
|
||||
"url": "https://pecl.php.net/get/mongodb-1.17.0.tgz"
|
||||
}
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.5",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "b5c6d7e8b5c6d7e8b5c6d7e8b5c6d7e8b5c6d7e8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPUnit\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/ext-mongodb@1.17.0",
|
||||
"purl": "pkg:composer/ext-mongodb@1.17.0",
|
||||
"name": "ext-mongodb",
|
||||
"version": "1.17.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.content_hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.type": "php-ext"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "ext-mongodb@1.17.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/ext-redis@6.0.2",
|
||||
"purl": "pkg:composer/ext-redis@6.0.2",
|
||||
"name": "ext-redis",
|
||||
"version": "6.0.2",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.content_hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.type": "php-ext"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "ext-redis@6.0.2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/mongodb/mongodb@1.17.0",
|
||||
"purl": "pkg:composer/mongodb/mongodb@1.17.0",
|
||||
"name": "mongodb/mongodb",
|
||||
"version": "1.17.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.files": "src/functions.php",
|
||||
"composer.autoload.psr4": "MongoDB\\->src/",
|
||||
"composer.content_hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "a4b5c6d7a4b5c6d7a4b5c6d7a4b5c6d7a4b5c6d7",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "mongodb/mongodb@1.17.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phpunit/phpunit@10.5.5",
|
||||
"purl": "pkg:composer/phpunit/phpunit@10.5.5",
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.5",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "PHPUnit\\->src/",
|
||||
"composer.content_hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "b5c6d7e8b5c6d7e8b5c6d7e8b5c6d7e8b5c6d7e8",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.test": "phpunit"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phpunit/phpunit@10.5.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/predis/predis@2.2.2",
|
||||
"purl": "pkg:composer/predis/predis@2.2.2",
|
||||
"name": "predis/predis",
|
||||
"version": "2.2.2",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Predis\\->src/",
|
||||
"composer.content_hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "f3a4b5c6f3a4b5c6f3a4b5c6f3a4b5c6f3a4b5c6",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "predis/predis@2.2.2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/psr/http-message@2.0.0",
|
||||
"purl": "pkg:composer/psr/http-message@2.0.0",
|
||||
"name": "psr/http-message",
|
||||
"version": "2.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Psr\\Http\\Message\\->src/",
|
||||
"composer.content_hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "e2f3a4b5e2f3a4b5e2f3a4b5e2f3a4b5e2f3a4b5",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "psr/http-message@2.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/slim/slim@4.12.0",
|
||||
"purl": "pkg:composer/slim/slim@4.12.0",
|
||||
"name": "slim/slim",
|
||||
"version": "4.12.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Slim\\->Slim",
|
||||
"composer.content_hash": "f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "d1e2f3a4d1e2f3a4d1e2f3a4d1e2f3a4d1e2f3a4",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.framework": "slim"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "slim/slim@4.12.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"content-hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"plugin-api-version": "2.6.0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "11.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "abcd1234abcd1234abcd1234abcd1234abcd1234"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/abcd1234",
|
||||
"shasum": "a1b2c3d4e5f6a7b8c9d0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Illuminate\\": "src/Illuminate"
|
||||
},
|
||||
"files": [
|
||||
"src/Illuminate/Foundation/helpers.php",
|
||||
"src/Illuminate/Support/helpers.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.8.1",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/guzzle.git",
|
||||
"reference": "ef5f8f89d0d0e3b8da1bb2e3a9c9d0a1b2c3d4e5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.5.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Seldaek/monolog.git",
|
||||
"reference": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Monolog\\": "src/Monolog"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Log\\": "src"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
"version": "5.6.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vlucas/phpdotenv.git",
|
||||
"reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dotenv\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "fedcba98fedcba98fedcba98fedcba98fedcba98"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPUnit\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"src/Framework/Assert.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mockery/mockery",
|
||||
"version": "1.6.7",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mockery/mockery.git",
|
||||
"reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mockery\\": "library/Mockery"
|
||||
},
|
||||
"files": [
|
||||
"library/helpers.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "1.23.1",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/FakerPHP/Faker.git",
|
||||
"reference": "bfb4fe148f18c79c45df6f9c6e19393795a2d07f"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Faker\\": "src/Faker/"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/fakerphp/faker@1.23.1",
|
||||
"purl": "pkg:composer/fakerphp/faker@1.23.1",
|
||||
"name": "fakerphp/faker",
|
||||
"version": "1.23.1",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Faker\\->src/Faker/",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "bfb4fe148f18c79c45df6f9c6e19393795a2d07f",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "fakerphp/faker@1.23.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/guzzlehttp/guzzle@7.8.1",
|
||||
"purl": "pkg:composer/guzzlehttp/guzzle@7.8.1",
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.8.1",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.files": "src/functions_include.php",
|
||||
"composer.autoload.psr4": "GuzzleHttp\\->src/",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "ef5f8f89d0d0e3b8da1bb2e3a9c9d0a1b2c3d4e5",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "guzzlehttp/guzzle@7.8.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/laravel/framework@11.0.0",
|
||||
"purl": "pkg:composer/laravel/framework@11.0.0",
|
||||
"name": "laravel/framework",
|
||||
"version": "11.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.files": "src/Illuminate/Foundation/helpers.php;src/Illuminate/Support/helpers.php",
|
||||
"composer.autoload.psr4": "Illuminate\\->src/Illuminate",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "false",
|
||||
"composer.dist.sha256": "a1b2c3d4e5f6a7b8c9d0",
|
||||
"composer.dist.url": "https://api.github.com/repos/laravel/framework/zipball/abcd1234",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.framework": "laravel"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "laravel/framework@11.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/mockery/mockery@1.6.7",
|
||||
"purl": "pkg:composer/mockery/mockery@1.6.7",
|
||||
"name": "mockery/mockery",
|
||||
"version": "1.6.7",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.files": "library/helpers.php",
|
||||
"composer.autoload.psr4": "Mockery\\->library/Mockery",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "mockery/mockery@1.6.7"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/monolog/monolog@3.5.0",
|
||||
"purl": "pkg:composer/monolog/monolog@3.5.0",
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.5.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Monolog\\->src/Monolog",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "monolog/monolog@3.5.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phpunit/phpunit@11.0.0",
|
||||
"purl": "pkg:composer/phpunit/phpunit@11.0.0",
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.classmap": "src/Framework/Assert.php",
|
||||
"composer.autoload.psr4": "PHPUnit\\->src/",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "fedcba98fedcba98fedcba98fedcba98fedcba98",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.test": "phpunit"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phpunit/phpunit@11.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/psr/log@3.0.0",
|
||||
"purl": "pkg:composer/psr/log@3.0.0",
|
||||
"name": "psr/log",
|
||||
"version": "3.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Psr\\Log\\->src",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "psr/log@3.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/vlucas/phpdotenv@5.6.0",
|
||||
"purl": "pkg:composer/vlucas/phpdotenv@5.6.0",
|
||||
"name": "vlucas/phpdotenv",
|
||||
"version": "5.6.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Dotenv\\->src/",
|
||||
"composer.content_hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "vlucas/phpdotenv@5.6.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"content-hash": "d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9",
|
||||
"plugin-api-version": "1.1.0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "zendframework/zend-mvc",
|
||||
"version": "2.7.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zendframework/zend-mvc.git",
|
||||
"reference": "a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Zend\\Mvc\\": "src/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pear/mail",
|
||||
"version": "1.6.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pear/Mail.git",
|
||||
"reference": "b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"Mail.php",
|
||||
"Mail/"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "5.2.28",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"class.phpmailer.php",
|
||||
"class.smtp.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"packages-dev": []
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/pear/mail@1.6.0",
|
||||
"purl": "pkg:composer/pear/mail@1.6.0",
|
||||
"name": "pear/mail",
|
||||
"version": "1.6.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.classmap": "Mail.php;Mail/",
|
||||
"composer.content_hash": "d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "1.1.0",
|
||||
"composer.source.ref": "b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5b2c3d4e5",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "pear/mail@1.6.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phpmailer/phpmailer@5.2.28",
|
||||
"purl": "pkg:composer/phpmailer/phpmailer@5.2.28",
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "5.2.28",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.classmap": "class.phpmailer.php;class.smtp.php",
|
||||
"composer.content_hash": "d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "1.1.0",
|
||||
"composer.source.ref": "c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6c3d4e5f6",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phpmailer/phpmailer@5.2.28"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/zendframework/zend-mvc@2.7.0",
|
||||
"purl": "pkg:composer/zendframework/zend-mvc@2.7.0",
|
||||
"name": "zendframework/zend-mvc",
|
||||
"version": "2.7.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.content_hash": "d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "1.1.0",
|
||||
"composer.source.ref": "a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "zendframework/zend-mvc@2.7.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
90
src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/Fixtures/lang/php/phar/composer.lock
generated
Normal file
90
src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/Fixtures/lang/php/phar/composer.lock
generated
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"content-hash": "e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0",
|
||||
"plugin-api-version": "2.6.0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.10.50",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "e1f2a3b4e1f2a3b4e1f2a3b4e1f2a3b4e1f2a3b4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e1f2a3b4",
|
||||
"shasum": "f1a2b3c4d5e6f1a2b3c4d5e6"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "composer/composer",
|
||||
"version": "2.6.6",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/composer.git",
|
||||
"reference": "f2a3b4c5f2a3b4c5f2a3b4c5f2a3b4c5f2a3b4c5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\": "src/Composer"
|
||||
}
|
||||
},
|
||||
"bin": [
|
||||
"bin/composer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
"version": "2.0.3",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phar-io/manifest.git",
|
||||
"reference": "a3b4c5d6a3b4c5d6a3b4c5d6a3b4c5d6a3b4c5d6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PharIo\\Manifest\\": "src/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "phar-io/version",
|
||||
"version": "3.2.1",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phar-io/version.git",
|
||||
"reference": "b4c5d6e7b4c5d6e7b4c5d6e7b4c5d6e7b4c5d6e7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PharIo\\Version\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.5",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "c5d6e7f8c5d6e7f8c5d6e7f8c5d6e7f8c5d6e7f8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPUnit\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/composer/composer@2.6.6",
|
||||
"purl": "pkg:composer/composer/composer@2.6.6",
|
||||
"name": "composer/composer",
|
||||
"version": "2.6.6",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Composer\\->src/Composer",
|
||||
"composer.content_hash": "e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "f2a3b4c5f2a3b4c5f2a3b4c5f2a3b4c5f2a3b4c5",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "composer/composer@2.6.6"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phar-io/manifest@2.0.3",
|
||||
"purl": "pkg:composer/phar-io/manifest@2.0.3",
|
||||
"name": "phar-io/manifest",
|
||||
"version": "2.0.3",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "PharIo\\Manifest\\->src/",
|
||||
"composer.content_hash": "e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "a3b4c5d6a3b4c5d6a3b4c5d6a3b4c5d6a3b4c5d6",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phar-io/manifest@2.0.3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phar-io/version@3.2.1",
|
||||
"purl": "pkg:composer/phar-io/version@3.2.1",
|
||||
"name": "phar-io/version",
|
||||
"version": "3.2.1",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "PharIo\\Version\\->src/",
|
||||
"composer.content_hash": "e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "b4c5d6e7b4c5d6e7b4c5d6e7b4c5d6e7b4c5d6e7",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phar-io/version@3.2.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phpstan/phpstan@1.10.50",
|
||||
"purl": "pkg:composer/phpstan/phpstan@1.10.50",
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.10.50",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.content_hash": "e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0",
|
||||
"composer.dev": "false",
|
||||
"composer.dist.sha256": "f1a2b3c4d5e6f1a2b3c4d5e6",
|
||||
"composer.dist.url": "https://api.github.com/repos/phpstan/phpstan/zipball/e1f2a3b4",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "e1f2a3b4e1f2a3b4e1f2a3b4e1f2a3b4e1f2a3b4",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phpstan/phpstan@1.10.50"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phpunit/phpunit@10.5.5",
|
||||
"purl": "pkg:composer/phpunit/phpunit@10.5.5",
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.5",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "PHPUnit\\->src/",
|
||||
"composer.content_hash": "e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "c5d6e7f8c5d6e7f8c5d6e7f8c5d6e7f8c5d6e7f8",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.test": "phpunit"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phpunit/phpunit@10.5.5"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
116
src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/Fixtures/lang/php/symfony/composer.lock
generated
Normal file
116
src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/Fixtures/lang/php/symfony/composer.lock
generated
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"content-hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"plugin-api-version": "2.6.0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "symfony/symfony",
|
||||
"version": "7.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/symfony.git",
|
||||
"reference": "1234abcd1234abcd1234abcd1234abcd1234abcd"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\": "src/Symfony"
|
||||
},
|
||||
"classmap": [
|
||||
"src/Symfony/Component/HttpKernel/Kernel.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "7.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "2345bcde2345bcde2345bcde2345bcde2345bcde"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Console\\": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "7.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-foundation.git",
|
||||
"reference": "3456cdef3456cdef3456cdef3456cdef3456cdef"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\HttpFoundation\\": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "doctrine/orm",
|
||||
"version": "3.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/orm.git",
|
||||
"reference": "4567def04567def04567def04567def04567def0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\ORM\\": "src"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "3.8.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "5678ef015678ef015678ef015678ef015678ef01"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Twig\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "6789f0126789f0126789f0126789f0126789f012"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPUnit\\": "src/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "symfony/phpunit-bridge",
|
||||
"version": "7.0.0",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/phpunit-bridge.git",
|
||||
"reference": "789a0123789a0123789a0123789a0123789a0123"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bridge\\PhpUnit\\": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/doctrine/orm@3.0.0",
|
||||
"purl": "pkg:composer/doctrine/orm@3.0.0",
|
||||
"name": "doctrine/orm",
|
||||
"version": "3.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Doctrine\\ORM\\->src",
|
||||
"composer.content_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "4567def04567def04567def04567def04567def0",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "doctrine/orm@3.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phpunit/phpunit@10.5.0",
|
||||
"purl": "pkg:composer/phpunit/phpunit@10.5.0",
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "PHPUnit\\->src/",
|
||||
"composer.content_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "6789f0126789f0126789f0126789f0126789f012",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.test": "phpunit"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phpunit/phpunit@10.5.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/symfony/console@7.0.0",
|
||||
"purl": "pkg:composer/symfony/console@7.0.0",
|
||||
"name": "symfony/console",
|
||||
"version": "7.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Symfony\\Component\\Console\\->",
|
||||
"composer.content_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "2345bcde2345bcde2345bcde2345bcde2345bcde",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "symfony/console@7.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/symfony/http-foundation@7.0.0",
|
||||
"purl": "pkg:composer/symfony/http-foundation@7.0.0",
|
||||
"name": "symfony/http-foundation",
|
||||
"version": "7.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Symfony\\Component\\HttpFoundation\\->",
|
||||
"composer.content_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "3456cdef3456cdef3456cdef3456cdef3456cdef",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "symfony/http-foundation@7.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/symfony/phpunit-bridge@7.0.0",
|
||||
"purl": "pkg:composer/symfony/phpunit-bridge@7.0.0",
|
||||
"name": "symfony/phpunit-bridge",
|
||||
"version": "7.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Symfony\\Bridge\\PhpUnit\\->",
|
||||
"composer.content_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "789a0123789a0123789a0123789a0123789a0123",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "symfony/phpunit-bridge@7.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/symfony/symfony@7.0.0",
|
||||
"purl": "pkg:composer/symfony/symfony@7.0.0",
|
||||
"name": "symfony/symfony",
|
||||
"version": "7.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.classmap": "src/Symfony/Component/HttpKernel/Kernel.php",
|
||||
"composer.autoload.psr4": "Symfony\\->src/Symfony",
|
||||
"composer.content_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "1234abcd1234abcd1234abcd1234abcd1234abcd",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.framework": "symfony"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "symfony/symfony@7.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/twig/twig@3.8.0",
|
||||
"purl": "pkg:composer/twig/twig@3.8.0",
|
||||
"name": "twig/twig",
|
||||
"version": "3.8.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Twig\\->src/",
|
||||
"composer.content_hash": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "5678ef015678ef015678ef015678ef015678ef01",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "twig/twig@3.8.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"content-hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8",
|
||||
"plugin-api-version": "2.6.0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "wordpress/wordpress",
|
||||
"version": "6.4.2",
|
||||
"type": "wordpress-core",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/WordPress/WordPress.git",
|
||||
"reference": "abcdef01abcdef01abcdef01abcdef01abcdef01"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"wp-includes/class-wp.php",
|
||||
"wp-includes/class-wp-query.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wpackagist-plugin/woocommerce",
|
||||
"version": "8.4.0",
|
||||
"type": "wordpress-plugin",
|
||||
"source": {
|
||||
"type": "svn",
|
||||
"url": "https://plugins.svn.wordpress.org/woocommerce/",
|
||||
"reference": "trunk"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Automattic\\WooCommerce\\": "src/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wpackagist-plugin/advanced-custom-fields",
|
||||
"version": "6.2.4",
|
||||
"type": "wordpress-plugin",
|
||||
"source": {
|
||||
"type": "svn",
|
||||
"url": "https://plugins.svn.wordpress.org/advanced-custom-fields/",
|
||||
"reference": "trunk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "johnpbloch/wordpress-core-installer",
|
||||
"version": "2.0.0",
|
||||
"type": "composer-plugin",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/johnpbloch/wordpress-core-installer.git",
|
||||
"reference": "0bcebe70c7a4c5d2bed0f5b82edbe4d28c13d97a"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"johnpbloch\\Composer\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "9.6.15",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "0e7b8d61a51b99a0be6d6cf3cfbc01a56a4a620e"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPUnit\\": "src/"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wp-phpunit/wp-phpunit",
|
||||
"version": "6.4.2",
|
||||
"type": "library",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/wp-phpunit/wp-phpunit.git",
|
||||
"reference": "8910abcd8910abcd8910abcd8910abcd8910abcd"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"includes/"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/johnpbloch/wordpress-core-installer@2.0.0",
|
||||
"purl": "pkg:composer/johnpbloch/wordpress-core-installer@2.0.0",
|
||||
"name": "johnpbloch/wordpress-core-installer",
|
||||
"version": "2.0.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "johnpbloch\\Composer\\->src/",
|
||||
"composer.content_hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "0bcebe70c7a4c5d2bed0f5b82edbe4d28c13d97a",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "composer-plugin"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "johnpbloch/wordpress-core-installer@2.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/phpunit/phpunit@9.6.15",
|
||||
"purl": "pkg:composer/phpunit/phpunit@9.6.15",
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "9.6.15",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "PHPUnit\\->src/",
|
||||
"composer.content_hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "0e7b8d61a51b99a0be6d6cf3cfbc01a56a4a620e",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library",
|
||||
"php.capability.test": "phpunit"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "phpunit/phpunit@9.6.15"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/wordpress/wordpress@6.4.2",
|
||||
"purl": "pkg:composer/wordpress/wordpress@6.4.2",
|
||||
"name": "wordpress/wordpress",
|
||||
"version": "6.4.2",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.classmap": "wp-includes/class-wp-query.php;wp-includes/class-wp.php",
|
||||
"composer.content_hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "abcdef01abcdef01abcdef01abcdef01abcdef01",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "wordpress-core",
|
||||
"php.capability.cms": "wordpress"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "wordpress/wordpress@6.4.2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/wp-phpunit/wp-phpunit@6.4.2",
|
||||
"purl": "pkg:composer/wp-phpunit/wp-phpunit@6.4.2",
|
||||
"name": "wp-phpunit/wp-phpunit",
|
||||
"version": "6.4.2",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.classmap": "includes/",
|
||||
"composer.content_hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8",
|
||||
"composer.dev": "true",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "8910abcd8910abcd8910abcd8910abcd8910abcd",
|
||||
"composer.source.type": "git",
|
||||
"composer.type": "library"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "wp-phpunit/wp-phpunit@6.4.2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/wpackagist-plugin/advanced-custom-fields@6.2.4",
|
||||
"purl": "pkg:composer/wpackagist-plugin/advanced-custom-fields@6.2.4",
|
||||
"name": "wpackagist-plugin/advanced-custom-fields",
|
||||
"version": "6.2.4",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.content_hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "trunk",
|
||||
"composer.source.type": "svn",
|
||||
"composer.type": "wordpress-plugin"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "wpackagist-plugin/advanced-custom-fields@6.2.4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "php",
|
||||
"componentKey": "purl::pkg:composer/wpackagist-plugin/woocommerce@8.4.0",
|
||||
"purl": "pkg:composer/wpackagist-plugin/woocommerce@8.4.0",
|
||||
"name": "wpackagist-plugin/woocommerce",
|
||||
"version": "8.4.0",
|
||||
"type": "composer",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"composer.autoload.psr4": "Automattic\\WooCommerce\\->src/",
|
||||
"composer.content_hash": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8",
|
||||
"composer.dev": "false",
|
||||
"composer.plugin_api_version": "2.6.0",
|
||||
"composer.source.ref": "trunk",
|
||||
"composer.source.type": "svn",
|
||||
"composer.type": "wordpress-plugin"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "composer.lock",
|
||||
"locator": "composer.lock",
|
||||
"value": "wpackagist-plugin/woocommerce@8.4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -6,6 +6,8 @@ namespace StellaOps.Scanner.Analyzers.Lang.Php.Tests;
|
||||
|
||||
public sealed class PhpLanguageAnalyzerTests
|
||||
{
|
||||
private static ILanguageAnalyzer[] CreateAnalyzers() => [new PhpLanguageAnalyzer()];
|
||||
|
||||
[Fact]
|
||||
public async Task ComposerLockPackagesAreEmittedAsync()
|
||||
{
|
||||
@@ -13,15 +15,94 @@ public sealed class PhpLanguageAnalyzerTests
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "php", "basic");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PhpLanguageAnalyzer()
|
||||
};
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
CreateAnalyzers(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LaravelExtendedFixtureAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "php", "laravel-extended");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
CreateAnalyzers(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SymfonyFixtureAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "php", "symfony");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
CreateAnalyzers(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WordPressFixtureAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "php", "wordpress");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
CreateAnalyzers(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyPhpFixtureAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "php", "legacy");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
CreateAnalyzers(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PharFixtureAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "php", "phar");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
CreateAnalyzers(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContainerFixtureAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "php", "container");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
CreateAnalyzers(),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Entrypoints;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.VirtualFileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Entrypoints;
|
||||
|
||||
public sealed class PythonEntrypointDiscoveryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsPackageMain()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
var packageDir = Path.Combine(sitePackages, "mypackage");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(packageDir, "__init__.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(packageDir, "__main__.py"), "print('Hello')", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.PackageMain &&
|
||||
e.Name == "mypackage" &&
|
||||
e.Target == "mypackage");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsConsoleScripts()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
var distInfo = Path.Combine(sitePackages, "mypackage-1.0.0.dist-info");
|
||||
Directory.CreateDirectory(distInfo);
|
||||
|
||||
var entryPoints = @"
|
||||
[console_scripts]
|
||||
myapp = mypackage.cli:main
|
||||
mytool = mypackage.tools:run
|
||||
|
||||
[gui_scripts]
|
||||
mygui = mypackage.gui:start
|
||||
";
|
||||
await File.WriteAllTextAsync(Path.Combine(distInfo, "entry_points.txt"), entryPoints, cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.ConsoleScript &&
|
||||
e.Name == "myapp" &&
|
||||
e.Target == "mypackage.cli:main");
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.ConsoleScript &&
|
||||
e.Name == "mytool" &&
|
||||
e.Target == "mypackage.tools:run");
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.GuiScript &&
|
||||
e.Name == "mygui" &&
|
||||
e.Target == "mypackage.gui:start");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsDjangoManage()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "manage.py"),
|
||||
"#!/usr/bin/env python\nimport django\n",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.DjangoManage &&
|
||||
e.Name == "manage.py");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsDjangoWsgiAsgi()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var projectDir = Path.Combine(tempPath, "myproject");
|
||||
Directory.CreateDirectory(projectDir);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(projectDir, "settings.py"),
|
||||
"DEBUG = True",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(projectDir, "wsgi.py"),
|
||||
"from django.core.wsgi import get_wsgi_application\napplication = get_wsgi_application()",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(projectDir, "asgi.py"),
|
||||
"from django.core.asgi import get_asgi_application\napplication = get_asgi_application()",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.WsgiApp &&
|
||||
e.Target == "myproject.wsgi:application");
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.AsgiApp &&
|
||||
e.Target == "myproject.asgi:application");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsLambdaHandler()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "lambda_function.py"),
|
||||
"def handler(event, context): return {'statusCode': 200}",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.LambdaHandler &&
|
||||
e.Target == "lambda_function.handler");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsStandaloneScriptsWithMainGuard()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "script.py"),
|
||||
"def main(): pass\n\nif __name__ == '__main__':\n main()",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.StandaloneScript &&
|
||||
e.Name == "script" &&
|
||||
e.Confidence == PythonEntrypointConfidence.High);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsClickCliApp()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var cliContent = @"
|
||||
import click
|
||||
|
||||
@click.command()
|
||||
def cli():
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
";
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "cli.py"), cliContent, cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.CliApp);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiscoverAsync_FindsProcfileEntrypoints()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "Procfile"),
|
||||
"web: gunicorn myapp.wsgi:application\nworker: celery -A myapp worker",
|
||||
cancellationToken);
|
||||
|
||||
// Need at least one Python file for VFS
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "dummy.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var discovery = new PythonEntrypointDiscovery(vfs, tempPath);
|
||||
await discovery.DiscoverAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(discovery.Entrypoints, e =>
|
||||
e.Kind == PythonEntrypointKind.WsgiApp &&
|
||||
e.Target == "myapp.wsgi:application" &&
|
||||
e.Source == "Procfile");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PythonEntrypointAnalysis_ReturnsOrganizedResults()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create a package with console script
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
var distInfo = Path.Combine(sitePackages, "mypackage-1.0.0.dist-info");
|
||||
Directory.CreateDirectory(distInfo);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(distInfo, "entry_points.txt"),
|
||||
"[console_scripts]\nmyapp = mypackage:main",
|
||||
cancellationToken);
|
||||
|
||||
// Create manage.py
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "manage.py"), "import django", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var analysis = await PythonEntrypointAnalysis.AnalyzeAsync(vfs, tempPath, cancellationToken);
|
||||
|
||||
Assert.NotEmpty(analysis.Entrypoints);
|
||||
Assert.NotEmpty(analysis.ConsoleScripts);
|
||||
Assert.NotEmpty(analysis.FrameworkEntrypoints);
|
||||
Assert.NotNull(analysis.PrimaryEntrypoint);
|
||||
|
||||
var metadata = analysis.ToMetadata().ToList();
|
||||
Assert.Contains(metadata, m => m.Key == "entrypoints.total");
|
||||
Assert.Contains(metadata, m => m.Key == "entrypoints.primary.name");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PythonEntrypoint_ModulePath_ExtractsCorrectly()
|
||||
{
|
||||
var entrypoint = new PythonEntrypoint(
|
||||
Name: "myapp",
|
||||
Kind: PythonEntrypointKind.ConsoleScript,
|
||||
Target: "mypackage.cli:main",
|
||||
VirtualPath: null,
|
||||
InvocationContext: PythonInvocationContext.AsConsoleScript("myapp"),
|
||||
Confidence: PythonEntrypointConfidence.Definitive,
|
||||
Source: "entry_points.txt");
|
||||
|
||||
Assert.Equal("mypackage.cli", entrypoint.ModulePath);
|
||||
Assert.Equal("main", entrypoint.Callable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PythonEntrypoint_IsFrameworkEntrypoint_DetectsCorrectly()
|
||||
{
|
||||
var djangoEntrypoint = new PythonEntrypoint(
|
||||
Name: "manage.py",
|
||||
Kind: PythonEntrypointKind.DjangoManage,
|
||||
Target: "manage",
|
||||
VirtualPath: "manage.py",
|
||||
InvocationContext: PythonInvocationContext.AsScript("manage.py"),
|
||||
Confidence: PythonEntrypointConfidence.High,
|
||||
Source: "manage.py");
|
||||
|
||||
var consoleScriptEntrypoint = new PythonEntrypoint(
|
||||
Name: "myapp",
|
||||
Kind: PythonEntrypointKind.ConsoleScript,
|
||||
Target: "mypackage:main",
|
||||
VirtualPath: null,
|
||||
InvocationContext: PythonInvocationContext.AsConsoleScript("myapp"),
|
||||
Confidence: PythonEntrypointConfidence.Definitive,
|
||||
Source: "entry_points.txt");
|
||||
|
||||
Assert.True(djangoEntrypoint.IsFrameworkEntrypoint);
|
||||
Assert.False(consoleScriptEntrypoint.IsFrameworkEntrypoint);
|
||||
Assert.True(consoleScriptEntrypoint.IsCliEntrypoint);
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-entrypoints-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Imports;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Imports;
|
||||
|
||||
public sealed class PythonImportExtractorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Extract_StandardImport_ParsesCorrectly()
|
||||
{
|
||||
var content = "import os";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("os", import.Module);
|
||||
Assert.Equal(PythonImportKind.Import, import.Kind);
|
||||
Assert.Null(import.Alias);
|
||||
Assert.False(import.IsRelative);
|
||||
Assert.Equal(PythonImportConfidence.Definitive, import.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_ImportWithAlias_ParsesCorrectly()
|
||||
{
|
||||
var content = "import numpy as np";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("numpy", import.Module);
|
||||
Assert.Equal("np", import.Alias);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_MultipleImports_ParsesAll()
|
||||
{
|
||||
var content = "import os, sys, json";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Equal(3, extractor.Imports.Count);
|
||||
Assert.Contains(extractor.Imports, i => i.Module == "os");
|
||||
Assert.Contains(extractor.Imports, i => i.Module == "sys");
|
||||
Assert.Contains(extractor.Imports, i => i.Module == "json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_FromImport_ParsesCorrectly()
|
||||
{
|
||||
var content = "from os.path import join, dirname";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("os.path", import.Module);
|
||||
Assert.Equal(PythonImportKind.FromImport, import.Kind);
|
||||
Assert.Equal(2, import.Names!.Count);
|
||||
Assert.Contains(import.Names, n => n.Name == "join");
|
||||
Assert.Contains(import.Names, n => n.Name == "dirname");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_FromImportWithAlias_ParsesCorrectly()
|
||||
{
|
||||
var content = "from collections import OrderedDict as OD";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("collections", import.Module);
|
||||
Assert.Single(import.Names!);
|
||||
Assert.Equal("OrderedDict", import.Names![0].Name);
|
||||
Assert.Equal("OD", import.Names![0].Alias);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_StarImport_ParsesCorrectly()
|
||||
{
|
||||
var content = "from os.path import *";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("os.path", import.Module);
|
||||
Assert.Equal(PythonImportKind.StarImport, import.Kind);
|
||||
Assert.True(import.IsStar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_RelativeImport_SingleDot_ParsesCorrectly()
|
||||
{
|
||||
var content = "from . import foo";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("", import.Module);
|
||||
Assert.Equal(1, import.RelativeLevel);
|
||||
Assert.True(import.IsRelative);
|
||||
Assert.Equal(PythonImportKind.RelativeImport, import.Kind);
|
||||
Assert.Equal(".", import.QualifiedModule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_RelativeImport_DoubleDot_ParsesCorrectly()
|
||||
{
|
||||
var content = "from ..utils import helper";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("utils", import.Module);
|
||||
Assert.Equal(2, import.RelativeLevel);
|
||||
Assert.True(import.IsRelative);
|
||||
Assert.Equal("..utils", import.QualifiedModule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_FutureImport_ParsesCorrectly()
|
||||
{
|
||||
var content = "from __future__ import annotations";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("__future__", import.Module);
|
||||
Assert.Equal(PythonImportKind.FutureImport, import.Kind);
|
||||
Assert.True(import.IsFuture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_ImportlibImportModule_ParsesCorrectly()
|
||||
{
|
||||
var content = "importlib.import_module('mymodule')";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("mymodule", import.Module);
|
||||
Assert.Equal(PythonImportKind.ImportlibImportModule, import.Kind);
|
||||
Assert.Equal(PythonImportConfidence.High, import.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_ImportlibImportModule_Relative_ParsesCorrectly()
|
||||
{
|
||||
var content = "importlib.import_module('.submodule', 'mypackage')";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("submodule", import.Module);
|
||||
Assert.Equal(1, import.RelativeLevel);
|
||||
Assert.Equal(PythonImportKind.ImportlibImportModule, import.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_BuiltinImport_ParsesCorrectly()
|
||||
{
|
||||
var content = "__import__('os')";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("os", import.Module);
|
||||
Assert.Equal(PythonImportKind.BuiltinImport, import.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_PkgutilExtendPath_ParsesCorrectly()
|
||||
{
|
||||
var content = "pkgutil.extend_path(__path__, __name__)";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal(PythonImportKind.PkgutilExtendPath, import.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_ConditionalImport_InTryBlock_MarkedAsConditional()
|
||||
{
|
||||
var content = @"
|
||||
try:
|
||||
import optional_module
|
||||
except ImportError:
|
||||
pass
|
||||
";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("optional_module", import.Module);
|
||||
Assert.True(import.IsConditional);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_LazyImport_InFunction_MarkedAsLazy()
|
||||
{
|
||||
var content = @"
|
||||
def my_function():
|
||||
import heavy_module
|
||||
return heavy_module.do_something()
|
||||
";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("heavy_module", import.Module);
|
||||
Assert.True(import.IsLazy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_TypeCheckingImport_MarkedCorrectly()
|
||||
{
|
||||
var content = @"
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mymodule import MyClass
|
||||
";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
// Should find the typing import and the TYPE_CHECKING import
|
||||
Assert.Equal(2, extractor.Imports.Count);
|
||||
|
||||
var typeCheckingImport = extractor.Imports.First(i => i.Module == "mymodule");
|
||||
Assert.True(typeCheckingImport.IsTypeCheckingOnly);
|
||||
Assert.Equal(PythonImportKind.TypeCheckingImport, typeCheckingImport.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_ParenthesizedImport_ParsesCorrectly()
|
||||
{
|
||||
var content = @"
|
||||
from mymodule import (
|
||||
Class1,
|
||||
Class2,
|
||||
function1
|
||||
)
|
||||
";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
var import = extractor.Imports[0];
|
||||
Assert.Equal("mymodule", import.Module);
|
||||
Assert.Equal(3, import.Names!.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_LineContinuation_ParsesCorrectly()
|
||||
{
|
||||
var content = @"import os, \
|
||||
sys, \
|
||||
json";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Equal(3, extractor.Imports.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_SkipsComments()
|
||||
{
|
||||
var content = @"
|
||||
# import not_imported
|
||||
import real_import # This is imported
|
||||
";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
Assert.Equal("real_import", extractor.Imports[0].Module);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_SkipsStringContents()
|
||||
{
|
||||
var content = @"
|
||||
code = 'import fake_import'
|
||||
import real_import
|
||||
";
|
||||
var extractor = new PythonSourceImportExtractor("test.py");
|
||||
extractor.Extract(content);
|
||||
|
||||
Assert.Single(extractor.Imports);
|
||||
Assert.Equal("real_import", extractor.Imports[0].Module);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PythonImport_ModulePath_ExtractsCorrectly()
|
||||
{
|
||||
var import = new PythonImport(
|
||||
Module: "mypackage.submodule",
|
||||
Names: [new PythonImportedName("MyClass")],
|
||||
Alias: null,
|
||||
Kind: PythonImportKind.FromImport,
|
||||
RelativeLevel: 0,
|
||||
SourceFile: "test.py",
|
||||
LineNumber: 1,
|
||||
Confidence: PythonImportConfidence.Definitive);
|
||||
|
||||
Assert.Equal("mypackage.submodule", import.Module);
|
||||
Assert.Equal("mypackage.submodule", import.QualifiedModule);
|
||||
Assert.Equal(["MyClass"], import.ImportedNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PythonImport_ToMetadata_GeneratesExpectedKeys()
|
||||
{
|
||||
var import = new PythonImport(
|
||||
Module: "os.path",
|
||||
Names: [new PythonImportedName("join"), new PythonImportedName("dirname")],
|
||||
Alias: null,
|
||||
Kind: PythonImportKind.FromImport,
|
||||
RelativeLevel: 0,
|
||||
SourceFile: "test.py",
|
||||
LineNumber: 5,
|
||||
Confidence: PythonImportConfidence.Definitive);
|
||||
|
||||
var metadata = import.ToMetadata(0).ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("os.path", metadata["import[0].module"]);
|
||||
Assert.Equal("FromImport", metadata["import[0].kind"]);
|
||||
Assert.Equal("Definitive", metadata["import[0].confidence"]);
|
||||
Assert.Equal("5", metadata["import[0].line"]);
|
||||
Assert.Equal("join,dirname", metadata["import[0].names"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Imports;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.VirtualFileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Imports;
|
||||
|
||||
public sealed class PythonImportGraphTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BuildAsync_DiscoversModules()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create a simple package structure
|
||||
var pkgDir = Path.Combine(tempPath, "mypackage");
|
||||
Directory.CreateDirectory(pkgDir);
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "module1.py"), "import os", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "module2.py"), "from . import module1", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(graph.Modules.Keys, k => k == "mypackage");
|
||||
Assert.Contains(graph.Modules.Keys, k => k == "mypackage.module1");
|
||||
Assert.Contains(graph.Modules.Keys, k => k == "mypackage.module2");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ExtractsImports()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var moduleContent = @"
|
||||
import os
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from . import other
|
||||
";
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "mymodule.py"),
|
||||
moduleContent,
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "other.py"),
|
||||
"# other module",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
var imports = graph.GetImportsForFile("mymodule.py");
|
||||
Assert.Equal(4, imports.Count);
|
||||
Assert.Contains(imports, i => i.Module == "os");
|
||||
Assert.Contains(imports, i => i.Module == "sys");
|
||||
Assert.Contains(imports, i => i.Module == "collections");
|
||||
Assert.Contains(imports, i => i.IsRelative);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_BuildsEdges()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create modules with dependencies
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "a.py"),
|
||||
"import b",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "b.py"),
|
||||
"import c",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "c.py"),
|
||||
"# no imports",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
// Check forward edges
|
||||
Assert.True(graph.Edges.ContainsKey("a"));
|
||||
Assert.Contains(graph.Edges["a"], e => e.To == "b");
|
||||
|
||||
Assert.True(graph.Edges.ContainsKey("b"));
|
||||
Assert.Contains(graph.Edges["b"], e => e.To == "c");
|
||||
|
||||
// Check reverse edges
|
||||
Assert.True(graph.ReverseEdges.ContainsKey("b"));
|
||||
Assert.Contains(graph.ReverseEdges["b"], e => e.From == "a");
|
||||
|
||||
Assert.True(graph.ReverseEdges.ContainsKey("c"));
|
||||
Assert.Contains(graph.ReverseEdges["c"], e => e.From == "b");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_ResolvesRelativeImports()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create package with relative imports
|
||||
var pkgDir = Path.Combine(tempPath, "mypackage");
|
||||
Directory.CreateDirectory(pkgDir);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(pkgDir, "__init__.py"),
|
||||
"from .module1 import func",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(pkgDir, "module1.py"),
|
||||
"def func(): pass",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(pkgDir, "module2.py"),
|
||||
"from . import module1\nfrom .module1 import func",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
// Check that mypackage imports mypackage.module1
|
||||
Assert.True(graph.Edges.ContainsKey("mypackage"));
|
||||
Assert.Contains(graph.Edges["mypackage"], e => e.To == "mypackage.module1");
|
||||
|
||||
// Check that module2 imports module1
|
||||
Assert.True(graph.Edges.ContainsKey("mypackage.module2"));
|
||||
Assert.Contains(graph.Edges["mypackage.module2"], e => e.To == "mypackage.module1");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_DetectsCycles()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create cyclic imports
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "a.py"),
|
||||
"import b",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "b.py"),
|
||||
"import c",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "c.py"),
|
||||
"import a",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
Assert.True(graph.HasCycle("a"));
|
||||
Assert.True(graph.HasCycle("b"));
|
||||
Assert.True(graph.HasCycle("c"));
|
||||
|
||||
var cycles = graph.FindCycles();
|
||||
Assert.NotEmpty(cycles);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_TopologicalOrder_NoCycles()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create acyclic dependencies
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "a.py"),
|
||||
"import b\nimport c",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "b.py"),
|
||||
"import c",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "c.py"),
|
||||
"# no imports",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
var order = graph.GetTopologicalOrder();
|
||||
Assert.NotNull(order);
|
||||
|
||||
// c should come before b and a
|
||||
var orderList = order.ToList();
|
||||
var cIndex = orderList.IndexOf("c");
|
||||
var bIndex = orderList.IndexOf("b");
|
||||
var aIndex = orderList.IndexOf("a");
|
||||
|
||||
Assert.True(cIndex < bIndex);
|
||||
Assert.True(cIndex < aIndex);
|
||||
Assert.True(bIndex < aIndex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildAsync_TopologicalOrder_WithCycles_ReturnsNull()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create cyclic imports
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "a.py"),
|
||||
"import b",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "b.py"),
|
||||
"import a",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
var order = graph.GetTopologicalOrder();
|
||||
Assert.Null(order);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDependencies_ReturnsDirectDependencies()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "a.py"),
|
||||
"import b\nimport c",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "b.py"),
|
||||
"",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "c.py"),
|
||||
"",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
var deps = graph.GetDependencies("a").ToList();
|
||||
Assert.Equal(2, deps.Count);
|
||||
Assert.Contains(deps, d => d.ModulePath == "b");
|
||||
Assert.Contains(deps, d => d.ModulePath == "c");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDependents_ReturnsModulesThatImportThis()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "a.py"),
|
||||
"import c",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "b.py"),
|
||||
"import c",
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "c.py"),
|
||||
"",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var graph = new PythonImportGraph(vfs, tempPath);
|
||||
await graph.BuildAsync(cancellationToken);
|
||||
|
||||
var dependents = graph.GetDependents("c").ToList();
|
||||
Assert.Equal(2, dependents.Count);
|
||||
Assert.Contains(dependents, d => d.ModulePath == "a");
|
||||
Assert.Contains(dependents, d => d.ModulePath == "b");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PythonImportAnalysis_CategoriesImports()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var content = @"
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from . import local_module
|
||||
from ..parent import something
|
||||
import importlib
|
||||
mod = importlib.import_module('dynamic')
|
||||
";
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "test.py"),
|
||||
content,
|
||||
cancellationToken);
|
||||
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "local_module.py"),
|
||||
"",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var analysis = await PythonImportAnalysis.AnalyzeAsync(vfs, tempPath, cancellationToken);
|
||||
|
||||
// Standard library imports
|
||||
Assert.Contains(analysis.StandardLibraryImports, i => i.Module == "os");
|
||||
Assert.Contains(analysis.StandardLibraryImports, i => i.Module == "sys");
|
||||
Assert.Contains(analysis.StandardLibraryImports, i => i.Module == "importlib");
|
||||
|
||||
// Third-party imports
|
||||
Assert.Contains(analysis.ThirdPartyImports, i => i.Module == "requests");
|
||||
|
||||
// Relative imports
|
||||
Assert.NotEmpty(analysis.RelativeImports);
|
||||
|
||||
// Dynamic imports
|
||||
Assert.Contains(analysis.DynamicImports, i => i.Kind == PythonImportKind.ImportlibImportModule);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PythonImportAnalysis_ToMetadata_GeneratesExpectedKeys()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(tempPath, "test.py"),
|
||||
"import os\nimport sys\nimport requests",
|
||||
cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var analysis = await PythonImportAnalysis.AnalyzeAsync(vfs, tempPath, cancellationToken);
|
||||
var metadata = analysis.ToMetadata().ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.True(metadata.ContainsKey("imports.total"));
|
||||
Assert.True(metadata.ContainsKey("imports.stdlib"));
|
||||
Assert.True(metadata.ContainsKey("imports.thirdParty"));
|
||||
Assert.True(metadata.ContainsKey("imports.modules"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PythonImportAnalysis_GetTransitiveDependencies_ReturnsAllDeps()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// a -> b -> c -> d
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "a.py"), "import b", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "b.py"), "import c", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "c.py"), "import d", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "d.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var analysis = await PythonImportAnalysis.AnalyzeAsync(vfs, tempPath, cancellationToken);
|
||||
var transitiveDeps = analysis.GetTransitiveDependencies("a");
|
||||
|
||||
Assert.Contains("b", transitiveDeps);
|
||||
Assert.Contains("c", transitiveDeps);
|
||||
Assert.Contains("d", transitiveDeps);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-imports-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -4,32 +4,32 @@ using System.Text.Json;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests;
|
||||
|
||||
public sealed class PythonLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests;
|
||||
|
||||
public sealed class PythonLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SimpleVenvFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "python", "simple-venv");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var usageHints = new LanguageUsageHints(new[]
|
||||
{
|
||||
Path.Combine(fixturePath, "bin", "simple-tool")
|
||||
});
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
|
||||
var usageHints = new LanguageUsageHints(new[]
|
||||
{
|
||||
Path.Combine(fixturePath, "bin", "simple-tool")
|
||||
});
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken,
|
||||
usageHints);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public sealed class PythonLanguageAnalyzerTests
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
@@ -199,6 +199,221 @@ public sealed class PythonLanguageAnalyzerTests
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsSitecustomizeStartupHooksAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create site-packages with sitecustomize.py
|
||||
var sitePackages = Path.Combine(fixturePath, "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var sitecustomizePath = Path.Combine(sitePackages, "sitecustomize.py");
|
||||
await File.WriteAllTextAsync(sitecustomizePath, "# Site customization\nprint('startup hook')", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify startup hooks metadata is present
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "startupHooks.detected", "true"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsPthFilesWithImportDirectivesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(fixturePath, "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
// Create a .pth file with import directive
|
||||
var pthPath = Path.Combine(sitePackages, "test-hooks.pth");
|
||||
await File.WriteAllTextAsync(pthPath, "import some_module\n/some/path", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify .pth import warning metadata is present
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "pthFiles.withImports.detected", "true"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsOciLayerSitePackagesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create OCI layer structure with packages
|
||||
var layersDir = Path.Combine(fixturePath, "layers", "layer1", "fs");
|
||||
var sitePackages = Path.Combine(layersDir, "usr", "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
// Create a package in the layer
|
||||
var packageDir = Path.Combine(sitePackages, "layered-pkg");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
var modulePath = Path.Combine(packageDir, "__init__.py");
|
||||
await File.WriteAllTextAsync(modulePath, "__version__ = \"1.0.0\"", cancellationToken);
|
||||
|
||||
var distInfoDir = Path.Combine(sitePackages, "layered-pkg-1.0.0.dist-info");
|
||||
Directory.CreateDirectory(distInfoDir);
|
||||
|
||||
var metadataPath = Path.Combine(distInfoDir, "METADATA");
|
||||
await File.WriteAllTextAsync(metadataPath, "Metadata-Version: 2.1\nName: layered-pkg\nVersion: 1.0.0", cancellationToken);
|
||||
|
||||
var wheelPath = Path.Combine(distInfoDir, "WHEEL");
|
||||
await File.WriteAllTextAsync(wheelPath, "Wheel-Version: 1.0", cancellationToken);
|
||||
|
||||
var recordPath = Path.Combine(distInfoDir, "RECORD");
|
||||
await File.WriteAllTextAsync(recordPath, "", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify the package from OCI layers was discovered
|
||||
var found = false;
|
||||
foreach (var component in root.EnumerateArray())
|
||||
{
|
||||
if (component.TryGetProperty("name", out var nameElement) &&
|
||||
string.Equals(nameElement.GetString(), "layered-pkg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(found, "Package from OCI layer should be discovered");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsPythonEnvironmentVariablesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create environment file with PYTHONPATH
|
||||
var envPath = Path.Combine(fixturePath, ".env");
|
||||
await File.WriteAllTextAsync(envPath, "PYTHONPATH=/app/lib:/app/vendor", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify PYTHONPATH warning metadata is present
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "env.pythonpath", "/app/lib:/app/vendor"));
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "env.pythonpath.warning", "PYTHONPATH is set; may affect module resolution"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsPyvenvConfigAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create pyvenv.cfg file
|
||||
var pyvenvPath = Path.Combine(fixturePath, "pyvenv.cfg");
|
||||
await File.WriteAllTextAsync(pyvenvPath, "home = /usr/local/bin\ninclude-system-site-packages = false", cancellationToken);
|
||||
|
||||
// Create a package
|
||||
await CreatePythonPackageAsync(fixturePath, "test-pkg", "1.0.0", cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
// Verify PYTHONHOME warning metadata is present (from pyvenv.cfg home)
|
||||
Assert.True(ComponentHasMetadata(root, "test-pkg", "env.pythonhome", "/usr/local/bin"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-python-{Guid.NewGuid():N}");
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.Resolver;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.VirtualFileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.Resolver;
|
||||
|
||||
public sealed class PythonModuleResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Resolve_BuiltinModule_ReturnsBuiltin()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "dummy.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
var result = resolver.Resolve("sys");
|
||||
|
||||
Assert.True(result.IsResolved);
|
||||
Assert.Equal(PythonResolutionKind.BuiltinModule, result.Kind);
|
||||
Assert.Equal(PythonResolutionConfidence.Definitive, result.Confidence);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_SourceModule_FindsModule()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "mymodule.py"), "# my module", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
var result = resolver.Resolve("mymodule");
|
||||
|
||||
Assert.True(result.IsResolved);
|
||||
Assert.Equal(PythonResolutionKind.SourceModule, result.Kind);
|
||||
Assert.Equal("mymodule.py", result.VirtualPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_Package_FindsPackage()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var pkgDir = Path.Combine(tempPath, "mypackage");
|
||||
Directory.CreateDirectory(pkgDir);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "module.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
var result = resolver.Resolve("mypackage");
|
||||
|
||||
Assert.True(result.IsResolved);
|
||||
Assert.Equal(PythonResolutionKind.Package, result.Kind);
|
||||
Assert.True(result.IsPackage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_SubModule_FindsSubModule()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var pkgDir = Path.Combine(tempPath, "mypackage");
|
||||
Directory.CreateDirectory(pkgDir);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "submodule.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
var result = resolver.Resolve("mypackage.submodule");
|
||||
|
||||
Assert.True(result.IsResolved);
|
||||
Assert.Equal(PythonResolutionKind.SourceModule, result.Kind);
|
||||
Assert.Equal("mypackage/submodule.py", result.VirtualPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_NamespacePackage_FindsNamespacePackage()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create a namespace package (directory without __init__.py)
|
||||
var pkgDir = Path.Combine(tempPath, "namespace_pkg");
|
||||
Directory.CreateDirectory(pkgDir);
|
||||
|
||||
// Add a submodule to make the directory discoverable
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "submodule.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
var result = resolver.Resolve("namespace_pkg");
|
||||
|
||||
Assert.True(result.IsResolved);
|
||||
Assert.Equal(PythonResolutionKind.NamespacePackage, result.Kind);
|
||||
Assert.True(result.IsNamespacePackage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolve_NotFound_ReturnsNotFound()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "dummy.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
var result = resolver.Resolve("nonexistent_module");
|
||||
|
||||
Assert.False(result.IsResolved);
|
||||
Assert.Equal(PythonResolutionKind.NotFound, result.Kind);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveRelative_Level1_ResolvesFromPackage()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var pkgDir = Path.Combine(tempPath, "mypackage");
|
||||
Directory.CreateDirectory(pkgDir);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "module1.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "module2.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
// from . import module2 (inside module1.py)
|
||||
var result = resolver.ResolveRelative("module2", 1, "mypackage.module1");
|
||||
|
||||
Assert.True(result.IsResolved);
|
||||
Assert.Equal("mypackage/module2.py", result.VirtualPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveRelative_Level2_ResolvesFromParentPackage()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create nested package structure
|
||||
var pkgDir = Path.Combine(tempPath, "mypackage");
|
||||
var subDir = Path.Combine(pkgDir, "subpackage");
|
||||
Directory.CreateDirectory(subDir);
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "__init__.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(pkgDir, "utils.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(subDir, "__init__.py"), "", cancellationToken);
|
||||
await File.WriteAllTextAsync(Path.Combine(subDir, "module.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
// from ..utils import something (inside subpackage/module.py)
|
||||
var result = resolver.ResolveRelative("utils", 2, "mypackage.subpackage.module");
|
||||
|
||||
Assert.True(result.IsResolved);
|
||||
Assert.Equal("mypackage/utils.py", result.VirtualPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessPthFiles_AddsPaths()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
// Create a .pth file
|
||||
var pthContent = @"
|
||||
# This is a comment
|
||||
./extra_path
|
||||
";
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(sitePackages, "extra.pth"),
|
||||
pthContent,
|
||||
cancellationToken);
|
||||
|
||||
// Create the extra path with a module
|
||||
var extraPath = Path.Combine(sitePackages, "extra_path");
|
||||
Directory.CreateDirectory(extraPath);
|
||||
await File.WriteAllTextAsync(Path.Combine(extraPath, "extra_module.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
// Check that the path was added
|
||||
Assert.Contains(resolver.SearchPaths, p => p.Kind == PythonSearchPathKind.PthFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsStandardLibraryModule_ReturnsTrue_ForStdlib()
|
||||
{
|
||||
Assert.True(PythonModuleResolver.IsStandardLibraryModule("os"));
|
||||
Assert.True(PythonModuleResolver.IsStandardLibraryModule("os.path"));
|
||||
Assert.True(PythonModuleResolver.IsStandardLibraryModule("sys"));
|
||||
Assert.True(PythonModuleResolver.IsStandardLibraryModule("json"));
|
||||
Assert.True(PythonModuleResolver.IsStandardLibraryModule("collections"));
|
||||
Assert.True(PythonModuleResolver.IsStandardLibraryModule("collections.abc"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsStandardLibraryModule_ReturnsFalse_ForThirdParty()
|
||||
{
|
||||
Assert.False(PythonModuleResolver.IsStandardLibraryModule("requests"));
|
||||
Assert.False(PythonModuleResolver.IsStandardLibraryModule("numpy"));
|
||||
Assert.False(PythonModuleResolver.IsStandardLibraryModule("flask"));
|
||||
Assert.False(PythonModuleResolver.IsStandardLibraryModule("django"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsBuiltinModule_ReturnsTrue_ForBuiltins()
|
||||
{
|
||||
Assert.True(PythonModuleResolver.IsBuiltinModule("sys"));
|
||||
Assert.True(PythonModuleResolver.IsBuiltinModule("builtins"));
|
||||
Assert.True(PythonModuleResolver.IsBuiltinModule("_thread"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PythonModuleResolution_ParentModule_ReturnsCorrectly()
|
||||
{
|
||||
var resolution = new PythonModuleResolution(
|
||||
ModuleName: "mypackage.subpackage.module",
|
||||
Kind: PythonResolutionKind.SourceModule,
|
||||
VirtualPath: "mypackage/subpackage/module.py",
|
||||
AbsolutePath: null,
|
||||
SearchPath: "",
|
||||
Source: PythonFileSource.SourceTree,
|
||||
Confidence: PythonResolutionConfidence.Definitive);
|
||||
|
||||
Assert.Equal("mypackage.subpackage", resolution.ParentModule);
|
||||
Assert.Equal("module", resolution.SimpleName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PythonModuleResolution_ToMetadata_GeneratesExpectedKeys()
|
||||
{
|
||||
var resolution = new PythonModuleResolution(
|
||||
ModuleName: "mymodule",
|
||||
Kind: PythonResolutionKind.SourceModule,
|
||||
VirtualPath: "mymodule.py",
|
||||
AbsolutePath: "/path/to/mymodule.py",
|
||||
SearchPath: "/path/to",
|
||||
Source: PythonFileSource.SourceTree,
|
||||
Confidence: PythonResolutionConfidence.Definitive);
|
||||
|
||||
var metadata = resolution.ToMetadata("resolution").ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
|
||||
Assert.Equal("mymodule", metadata["resolution.module"]);
|
||||
Assert.Equal("SourceModule", metadata["resolution.kind"]);
|
||||
Assert.Equal("Definitive", metadata["resolution.confidence"]);
|
||||
Assert.Equal("mymodule.py", metadata["resolution.path"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolver_CachesResults()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(Path.Combine(tempPath, "mymodule.py"), "", cancellationToken);
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSourceTree(tempPath)
|
||||
.Build();
|
||||
|
||||
var resolver = new PythonModuleResolver(vfs, tempPath);
|
||||
await resolver.InitializeAsync(cancellationToken);
|
||||
|
||||
var result1 = resolver.Resolve("mymodule");
|
||||
var result2 = resolver.Resolve("mymodule");
|
||||
|
||||
Assert.Same(result1, result2);
|
||||
|
||||
var (total, resolved, notFound, cached) = resolver.GetStatistics();
|
||||
Assert.Equal(1, total);
|
||||
Assert.Equal(1, resolved);
|
||||
Assert.Equal(0, notFound);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-resolver-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@
|
||||
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||
<!-- Exclude OpenSSL shim files - already provided by StellaOps.Scanner.Analyzers.Lang.Tests reference -->
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslLegacyShim.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslAutoInit.cs" />
|
||||
<Using Remove="StellaOps.Concelier.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.VirtualFileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.VirtualFileSystem;
|
||||
|
||||
public sealed class PythonInputNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsVirtualenvLayout()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create pyvenv.cfg
|
||||
var pyvenvCfg = Path.Combine(tempPath, "pyvenv.cfg");
|
||||
await File.WriteAllTextAsync(pyvenvCfg, @"
|
||||
home = /usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.11.4
|
||||
", cancellationToken);
|
||||
|
||||
// Create lib/python3.11/site-packages
|
||||
var sitePackages = Path.Combine(tempPath, "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
Assert.Equal(PythonLayoutKind.Virtualenv, normalizer.Layout);
|
||||
Assert.Equal(tempPath, normalizer.VenvPath);
|
||||
Assert.Single(normalizer.SitePackagesPaths);
|
||||
Assert.Contains(normalizer.VersionTargets, v => v.Version == "3.11.4");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsPoetryLayout()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create poetry.lock
|
||||
var poetryLock = Path.Combine(tempPath, "poetry.lock");
|
||||
await File.WriteAllTextAsync(poetryLock, @"
|
||||
[[package]]
|
||||
name = ""requests""
|
||||
version = ""2.28.0""
|
||||
", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
Assert.Equal(PythonLayoutKind.Poetry, normalizer.Layout);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsPipenvLayout()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create Pipfile.lock
|
||||
var pipfileLock = Path.Combine(tempPath, "Pipfile.lock");
|
||||
await File.WriteAllTextAsync(pipfileLock, @"{""default"": {}, ""develop"": {}}", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
Assert.Equal(PythonLayoutKind.Pipenv, normalizer.Layout);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsCondaLayout()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create conda-meta directory
|
||||
var condaMeta = Path.Combine(tempPath, "conda-meta");
|
||||
Directory.CreateDirectory(condaMeta);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
Assert.Equal(PythonLayoutKind.Conda, normalizer.Layout);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsLambdaLayout()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create lambda_function.py
|
||||
var lambdaFunction = Path.Combine(tempPath, "lambda_function.py");
|
||||
await File.WriteAllTextAsync(lambdaFunction, "def handler(event, context): pass", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
Assert.Equal(PythonLayoutKind.Lambda, normalizer.Layout);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVersionFromPyprojectToml()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var pyproject = Path.Combine(tempPath, "pyproject.toml");
|
||||
await File.WriteAllTextAsync(pyproject, @"
|
||||
[project]
|
||||
name = ""mypackage""
|
||||
requires-python = "">=3.10""
|
||||
|
||||
[tool.poetry]
|
||||
python = ""^3.11""
|
||||
", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
Assert.Contains(normalizer.VersionTargets, v => v.Version == "3.10" && v.IsMinimum);
|
||||
Assert.Contains(normalizer.VersionTargets, v => v.Version == "3.11" && v.IsMinimum);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVersionFromRuntimeTxt()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var runtimeTxt = Path.Combine(tempPath, "runtime.txt");
|
||||
await File.WriteAllTextAsync(runtimeTxt, "python-3.11.4", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
var target = normalizer.VersionTargets.FirstOrDefault(v => v.Source == "runtime.txt");
|
||||
Assert.NotNull(target);
|
||||
Assert.Equal("3.11.4", target.Version);
|
||||
Assert.Equal(PythonVersionConfidence.Definitive, target.Confidence);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVersionFromDockerfile()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var dockerfile = Path.Combine(tempPath, "Dockerfile");
|
||||
await File.WriteAllTextAsync(dockerfile, @"
|
||||
FROM python:3.12.1-slim
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
var target = normalizer.VersionTargets.FirstOrDefault(v => v.Source == "Dockerfile");
|
||||
Assert.NotNull(target);
|
||||
Assert.Equal("3.12.1", target.Version);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ExtractsVersionFromSetupPy()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var setupPy = Path.Combine(tempPath, "setup.py");
|
||||
await File.WriteAllTextAsync(setupPy, @"
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='mypackage',
|
||||
python_requires='>=3.9',
|
||||
)
|
||||
", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
var target = normalizer.VersionTargets.FirstOrDefault(v => v.Source == "setup.py");
|
||||
Assert.NotNull(target);
|
||||
Assert.Equal("3.9", target.Version);
|
||||
Assert.True(target.IsMinimum);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsSitePackagesInMultipleLocations()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create pyvenv.cfg to establish venv
|
||||
var pyvenvCfg = Path.Combine(tempPath, "pyvenv.cfg");
|
||||
await File.WriteAllTextAsync(pyvenvCfg, "version = 3.11.0", cancellationToken);
|
||||
|
||||
// Create multiple site-packages locations
|
||||
var sitePackages1 = Path.Combine(tempPath, "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages1);
|
||||
|
||||
var sitePackages2 = Path.Combine(tempPath, "lib", "python3.12", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages2);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
Assert.Equal(2, normalizer.SitePackagesPaths.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsWheelsInDistDirectory()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create dist directory with wheels
|
||||
var distDir = Path.Combine(tempPath, "dist");
|
||||
Directory.CreateDirectory(distDir);
|
||||
|
||||
var wheelPath = Path.Combine(distDir, "mypackage-1.0.0-py3-none-any.whl");
|
||||
await File.WriteAllBytesAsync(wheelPath, Array.Empty<byte>(), cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
var vfs = normalizer.BuildVirtualFileSystem();
|
||||
// VFS won't have files from empty wheel, but the wheel was detected
|
||||
Assert.NotNull(vfs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DetectsZipapps()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create a .pyz file
|
||||
var pyzPath = Path.Combine(tempPath, "myapp.pyz");
|
||||
await File.WriteAllBytesAsync(pyzPath, Array.Empty<byte>(), cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
// Zipapp was detected (even if empty)
|
||||
var vfs = normalizer.BuildVirtualFileSystem();
|
||||
Assert.NotNull(vfs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_PrimaryVersionTargetHasHighestConfidence()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create pyvenv.cfg (Definitive confidence)
|
||||
var pyvenvCfg = Path.Combine(tempPath, "pyvenv.cfg");
|
||||
await File.WriteAllTextAsync(pyvenvCfg, "version = 3.11.4", cancellationToken);
|
||||
|
||||
// Create pyproject.toml (High confidence)
|
||||
var pyproject = Path.Combine(tempPath, "pyproject.toml");
|
||||
await File.WriteAllTextAsync(pyproject, @"[project]
|
||||
requires-python = "">=3.10""", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
|
||||
var primary = normalizer.PrimaryVersionTarget;
|
||||
Assert.NotNull(primary);
|
||||
Assert.Equal("3.11.4", primary.Version);
|
||||
Assert.Equal(PythonVersionConfidence.Definitive, primary.Confidence);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildVirtualFileSystem_IncludesAllDetectedSources()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
// Create pyvenv.cfg
|
||||
var pyvenvCfg = Path.Combine(tempPath, "pyvenv.cfg");
|
||||
await File.WriteAllTextAsync(pyvenvCfg, "version = 3.11.0", cancellationToken);
|
||||
|
||||
// Create site-packages with a package
|
||||
var sitePackages = Path.Combine(tempPath, "lib", "python3.11", "site-packages");
|
||||
var packageDir = Path.Combine(sitePackages, "mypackage");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
await File.WriteAllTextAsync(Path.Combine(packageDir, "__init__.py"), "", cancellationToken);
|
||||
|
||||
// Create bin directory
|
||||
var binDir = Path.Combine(tempPath, "bin");
|
||||
Directory.CreateDirectory(binDir);
|
||||
await File.WriteAllTextAsync(Path.Combine(binDir, "mytool"), "#!/usr/bin/env python", cancellationToken);
|
||||
|
||||
var normalizer = new PythonInputNormalizer(tempPath);
|
||||
await normalizer.AnalyzeAsync(cancellationToken);
|
||||
var vfs = normalizer.BuildVirtualFileSystem();
|
||||
|
||||
Assert.True(vfs.FileExists("mypackage/__init__.py"));
|
||||
Assert.True(vfs.FileExists("bin/mytool"));
|
||||
|
||||
var sitePackagesFiles = vfs.GetFilesBySource(PythonFileSource.SitePackages).ToArray();
|
||||
var binFiles = vfs.GetFilesBySource(PythonFileSource.VenvBin).ToArray();
|
||||
|
||||
Assert.Single(sitePackagesFiles);
|
||||
Assert.Single(binFiles);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PythonProjectAnalysis_AnalyzeAsync_ReturnsCompleteAnalysis()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var pyvenvCfg = Path.Combine(tempPath, "pyvenv.cfg");
|
||||
await File.WriteAllTextAsync(pyvenvCfg, "version = 3.11.0", cancellationToken);
|
||||
|
||||
var sitePackages = Path.Combine(tempPath, "lib", "python3.11", "site-packages");
|
||||
var packageDir = Path.Combine(sitePackages, "pkg");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
await File.WriteAllTextAsync(Path.Combine(packageDir, "__init__.py"), "", cancellationToken);
|
||||
|
||||
var analysis = await PythonProjectAnalysis.AnalyzeAsync(tempPath, cancellationToken);
|
||||
|
||||
Assert.Equal(PythonLayoutKind.Virtualenv, analysis.Layout);
|
||||
Assert.NotNull(analysis.PrimaryVersionTarget);
|
||||
Assert.Equal("3.11.0", analysis.PrimaryVersionTarget.Version);
|
||||
Assert.NotNull(analysis.VirtualFileSystem);
|
||||
Assert.True(analysis.VirtualFileSystem.FileCount > 0);
|
||||
|
||||
var metadata = analysis.ToMetadata().ToList();
|
||||
Assert.Contains(metadata, m => m.Key == "layout" && m.Value == "Virtualenv");
|
||||
Assert.Contains(metadata, m => m.Key == "pythonVersion" && m.Value == "3.11.0");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-normalizer-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
using System.IO.Compression;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python.Internal.VirtualFileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests.VirtualFileSystem;
|
||||
|
||||
public sealed class PythonVirtualFileSystemTests
|
||||
{
|
||||
[Fact]
|
||||
public void Builder_AddSitePackages_DiscoversPythonFiles()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var packageDir = Path.Combine(sitePackages, "mypackage");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
File.WriteAllText(Path.Combine(packageDir, "__init__.py"), "");
|
||||
File.WriteAllText(Path.Combine(packageDir, "core.py"), "def main(): pass");
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
Assert.Equal(2, vfs.FileCount);
|
||||
Assert.True(vfs.FileExists("mypackage/__init__.py"));
|
||||
Assert.True(vfs.FileExists("mypackage/core.py"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_AddSitePackages_SkipsPycacheDirectories()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var packageDir = Path.Combine(sitePackages, "mypackage");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
File.WriteAllText(Path.Combine(packageDir, "__init__.py"), "");
|
||||
|
||||
var pycacheDir = Path.Combine(packageDir, "__pycache__");
|
||||
Directory.CreateDirectory(pycacheDir);
|
||||
File.WriteAllText(Path.Combine(pycacheDir, "__init__.cpython-311.pyc"), "bytecode");
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
Assert.Equal(1, vfs.FileCount);
|
||||
Assert.True(vfs.FileExists("mypackage/__init__.py"));
|
||||
Assert.False(vfs.FileExists("mypackage/__pycache__/__init__.cpython-311.pyc"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_AddWheel_ExtractsArchiveContents()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var wheelPath = Path.Combine(tempPath, "mypackage-1.0.0-py3-none-any.whl");
|
||||
|
||||
using (var archive = ZipFile.Open(wheelPath, ZipArchiveMode.Create))
|
||||
{
|
||||
var entry = archive.CreateEntry("mypackage/__init__.py");
|
||||
using var writer = new StreamWriter(entry.Open());
|
||||
writer.Write("# Package init");
|
||||
|
||||
entry = archive.CreateEntry("mypackage/core.py");
|
||||
using var writer2 = new StreamWriter(entry.Open());
|
||||
writer2.Write("def main(): pass");
|
||||
|
||||
entry = archive.CreateEntry("mypackage-1.0.0.dist-info/METADATA");
|
||||
using var writer3 = new StreamWriter(entry.Open());
|
||||
writer3.Write("Name: mypackage\nVersion: 1.0.0");
|
||||
}
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddWheel(wheelPath)
|
||||
.Build();
|
||||
|
||||
Assert.Equal(3, vfs.FileCount);
|
||||
Assert.True(vfs.FileExists("mypackage/__init__.py"));
|
||||
Assert.True(vfs.FileExists("mypackage/core.py"));
|
||||
Assert.True(vfs.FileExists("mypackage-1.0.0.dist-info/METADATA"));
|
||||
|
||||
Assert.True(vfs.TryGetFile("mypackage/__init__.py", out var file));
|
||||
Assert.Equal(PythonFileSource.Wheel, file!.Source);
|
||||
Assert.Equal(wheelPath, file.ArchivePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_AddZipapp_ExtractsAfterShebang()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var zipappPath = Path.Combine(tempPath, "app.pyz");
|
||||
|
||||
// Create a zipapp with shebang
|
||||
using (var fileStream = File.Create(zipappPath))
|
||||
{
|
||||
// Write shebang
|
||||
var shebang = "#!/usr/bin/env python3\n"u8.ToArray();
|
||||
fileStream.Write(shebang, 0, shebang.Length);
|
||||
|
||||
// Write zip archive
|
||||
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, leaveOpen: true);
|
||||
var entry = archive.CreateEntry("__main__.py");
|
||||
using var writer = new StreamWriter(entry.Open());
|
||||
writer.Write("print('Hello from zipapp')");
|
||||
}
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddZipapp(zipappPath)
|
||||
.Build();
|
||||
|
||||
Assert.Equal(1, vfs.FileCount);
|
||||
Assert.True(vfs.FileExists("__main__.py"));
|
||||
|
||||
Assert.True(vfs.TryGetFile("__main__.py", out var file));
|
||||
Assert.Equal(PythonFileSource.Zipapp, file!.Source);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VirtualFileSystem_EnumerateFiles_ReturnsFilesInDirectory()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var packageDir = Path.Combine(sitePackages, "mypackage");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
File.WriteAllText(Path.Combine(packageDir, "__init__.py"), "");
|
||||
File.WriteAllText(Path.Combine(packageDir, "core.py"), "");
|
||||
File.WriteAllText(Path.Combine(packageDir, "utils.py"), "");
|
||||
|
||||
var subDir = Path.Combine(packageDir, "sub");
|
||||
Directory.CreateDirectory(subDir);
|
||||
File.WriteAllText(Path.Combine(subDir, "nested.py"), "");
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
var files = vfs.EnumerateFiles("mypackage").ToArray();
|
||||
|
||||
Assert.Equal(3, files.Length);
|
||||
Assert.Contains(files, f => f.FileName == "__init__.py");
|
||||
Assert.Contains(files, f => f.FileName == "core.py");
|
||||
Assert.Contains(files, f => f.FileName == "utils.py");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VirtualFileSystem_EnumerateFilesWithGlob_MatchesPattern()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var packageDir = Path.Combine(sitePackages, "mypackage");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
File.WriteAllText(Path.Combine(packageDir, "__init__.py"), "");
|
||||
File.WriteAllText(Path.Combine(packageDir, "core.py"), "");
|
||||
File.WriteAllText(Path.Combine(packageDir, "README.md"), "");
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
var pyFiles = vfs.EnumerateFiles("mypackage", "*.py").ToArray();
|
||||
|
||||
Assert.Equal(2, pyFiles.Length);
|
||||
Assert.All(pyFiles, f => Assert.True(f.IsPythonSource));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VirtualFileSystem_GetFilesBySource_FiltersCorrectly()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var packageDir = Path.Combine(sitePackages, "installed");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
File.WriteAllText(Path.Combine(packageDir, "__init__.py"), "");
|
||||
|
||||
var wheelPath = Path.Combine(tempPath, "wheel-1.0.0-py3-none-any.whl");
|
||||
using (var archive = ZipFile.Open(wheelPath, ZipArchiveMode.Create))
|
||||
{
|
||||
var entry = archive.CreateEntry("wheel/__init__.py");
|
||||
using var writer = new StreamWriter(entry.Open());
|
||||
writer.Write("");
|
||||
}
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.AddWheel(wheelPath)
|
||||
.Build();
|
||||
|
||||
var sitePackagesFiles = vfs.GetFilesBySource(PythonFileSource.SitePackages).ToArray();
|
||||
var wheelFiles = vfs.GetFilesBySource(PythonFileSource.Wheel).ToArray();
|
||||
|
||||
Assert.Single(sitePackagesFiles);
|
||||
Assert.Single(wheelFiles);
|
||||
Assert.Equal("installed/__init__.py", sitePackagesFiles[0].VirtualPath);
|
||||
Assert.Equal("wheel/__init__.py", wheelFiles[0].VirtualPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VirtualFileSystem_DirectoryExists_ReturnsTrueForDirectories()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages = Path.Combine(tempPath, "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var packageDir = Path.Combine(sitePackages, "mypackage", "sub");
|
||||
Directory.CreateDirectory(packageDir);
|
||||
File.WriteAllText(Path.Combine(packageDir, "nested.py"), "");
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages)
|
||||
.Build();
|
||||
|
||||
Assert.True(vfs.DirectoryExists("mypackage"));
|
||||
Assert.True(vfs.DirectoryExists("mypackage/sub"));
|
||||
Assert.False(vfs.DirectoryExists("mypackage/nonexistent"));
|
||||
Assert.False(vfs.DirectoryExists("other"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PythonVirtualFile_Properties_DetectFileTypes()
|
||||
{
|
||||
var pyFile = new PythonVirtualFile("pkg/__init__.py", "/path/to/file.py", PythonFileSource.SitePackages);
|
||||
var pywFile = new PythonVirtualFile("pkg/gui.pyw", "/path/to/gui.pyw", PythonFileSource.SitePackages);
|
||||
var pycFile = new PythonVirtualFile("pkg/__pycache__/init.cpython-311.pyc", "/path/to/file.pyc", PythonFileSource.SitePackages);
|
||||
var soFile = new PythonVirtualFile("pkg/_native.so", "/path/to/_native.so", PythonFileSource.SitePackages);
|
||||
var pydFile = new PythonVirtualFile("pkg/_native.pyd", "/path/to/_native.pyd", PythonFileSource.SitePackages);
|
||||
|
||||
Assert.True(pyFile.IsPythonSource);
|
||||
Assert.True(pywFile.IsPythonSource);
|
||||
Assert.False(pycFile.IsPythonSource);
|
||||
Assert.True(pycFile.IsBytecode);
|
||||
Assert.True(soFile.IsNativeExtension);
|
||||
Assert.True(pydFile.IsNativeExtension);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_LaterAdditionsOverrideEarlier()
|
||||
{
|
||||
var tempPath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
var sitePackages1 = Path.Combine(tempPath, "site-packages1");
|
||||
Directory.CreateDirectory(sitePackages1);
|
||||
|
||||
var packageDir1 = Path.Combine(sitePackages1, "pkg");
|
||||
Directory.CreateDirectory(packageDir1);
|
||||
File.WriteAllText(Path.Combine(packageDir1, "module.py"), "version = 1");
|
||||
|
||||
var sitePackages2 = Path.Combine(tempPath, "site-packages2");
|
||||
Directory.CreateDirectory(sitePackages2);
|
||||
|
||||
var packageDir2 = Path.Combine(sitePackages2, "pkg");
|
||||
Directory.CreateDirectory(packageDir2);
|
||||
File.WriteAllText(Path.Combine(packageDir2, "module.py"), "version = 2");
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
.AddSitePackages(sitePackages1)
|
||||
.AddSitePackages(sitePackages2)
|
||||
.Build();
|
||||
|
||||
// Should have the file from site-packages2 (later)
|
||||
Assert.True(vfs.TryGetFile("pkg/module.py", out var file));
|
||||
Assert.Contains("site-packages2", file!.AbsolutePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-vfs-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
"ruby.observation.capability.serialization": "false",
|
||||
"ruby.observation.dependency_edges": "6",
|
||||
"ruby.observation.packages": "9",
|
||||
"ruby.observation.ruby_version": "3.2.0",
|
||||
"ruby.observation.runtime_edges": "0"
|
||||
},
|
||||
"evidence": [
|
||||
@@ -20,8 +21,8 @@
|
||||
"kind": "derived",
|
||||
"source": "ruby.observation",
|
||||
"locator": "document",
|
||||
"value": "{\u0022$schema\u0022:\u0022stellaops.ruby.observation@1\u0022,\u0022packages\u0022:[{\u0022name\u0022:\u0022bundler\u0022,\u0022version\u0022:\u00222.5.3\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022development\u0022]},{\u0022name\u0022:\u0022pastel\u0022,\u0022version\u0022:\u00220.8.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022thor\u0022,\u0022version\u0022:\u00221.3.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-color\u0022,\u0022version\u0022:\u00220.6.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-cursor\u0022,\u0022version\u0022:\u00220.7.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-prompt\u0022,\u0022version\u0022:\u00220.23.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-reader\u0022,\u0022version\u0022:\u00220.9.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-screen\u0022,\u0022version\u0022:\u00220.8.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022wisper\u0022,\u0022version\u0022:\u00222.0.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]}],\u0022entrypoints\u0022:[],\u0022dependencyEdges\u0022:[{\u0022from\u0022:\u0022pkg:gem/pastel@0.8.0\u0022,\u0022to\u0022:\u0022tty-color\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.5\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-prompt@0.23.1\u0022,\u0022to\u0022:\u0022pastel\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.8\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-prompt@0.23.1\u0022,\u0022to\u0022:\u0022tty-reader\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.8\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-reader@0.9.0\u0022,\u0022to\u0022:\u0022tty-cursor\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.7\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-reader@0.9.0\u0022,\u0022to\u0022:\u0022tty-screen\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.8\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-reader@0.9.0\u0022,\u0022to\u0022:\u0022wisper\u0022,\u0022constraint\u0022:\u0022~\\u003E 2.0\u0022}],\u0022runtimeEdges\u0022:[],\u0022environment\u0022:{\u0022bundlerVersion\u0022:\u00222.5.3\u0022,\u0022lockfiles\u0022:[\u0022Gemfile.lock\u0022]},\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:false,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[]},\u0022bundledWith\u0022:\u00222.5.3\u0022}",
|
||||
"sha256": "sha256:5ec8b45dc480086cefbee03575845d57fb9fe4a0b000b109af46af5f2fe3f05d"
|
||||
"value": "{\u0022$schema\u0022:\u0022stellaops.ruby.observation@1\u0022,\u0022packages\u0022:[{\u0022name\u0022:\u0022bundler\u0022,\u0022version\u0022:\u00222.5.3\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022development\u0022]},{\u0022name\u0022:\u0022pastel\u0022,\u0022version\u0022:\u00220.8.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022thor\u0022,\u0022version\u0022:\u00221.3.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-color\u0022,\u0022version\u0022:\u00220.6.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-cursor\u0022,\u0022version\u0022:\u00220.7.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-prompt\u0022,\u0022version\u0022:\u00220.23.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-reader\u0022,\u0022version\u0022:\u00220.9.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022tty-screen\u0022,\u0022version\u0022:\u00220.8.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022wisper\u0022,\u0022version\u0022:\u00222.0.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]}],\u0022entrypoints\u0022:[],\u0022dependencyEdges\u0022:[{\u0022from\u0022:\u0022pkg:gem/pastel@0.8.0\u0022,\u0022to\u0022:\u0022tty-color\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.5\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-prompt@0.23.1\u0022,\u0022to\u0022:\u0022pastel\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.8\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-prompt@0.23.1\u0022,\u0022to\u0022:\u0022tty-reader\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.8\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-reader@0.9.0\u0022,\u0022to\u0022:\u0022tty-cursor\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.7\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-reader@0.9.0\u0022,\u0022to\u0022:\u0022tty-screen\u0022,\u0022constraint\u0022:\u0022~\\u003E 0.8\u0022},{\u0022from\u0022:\u0022pkg:gem/tty-reader@0.9.0\u0022,\u0022to\u0022:\u0022wisper\u0022,\u0022constraint\u0022:\u0022~\\u003E 2.0\u0022}],\u0022runtimeEdges\u0022:[],\u0022environment\u0022:{\u0022rubyVersion\u0022:\u00223.2.0\u0022,\u0022bundlerVersion\u0022:\u00222.5.3\u0022,\u0022lockfiles\u0022:[\u0022Gemfile.lock\u0022],\u0022rubyVersionSources\u0022:[{\u0022version\u0022:\u00223.2.0\u0022,\u0022source\u0022:\u0022Gemfile\u0022,\u0022sourceType\u0022:\u0022gemfile\u0022}]},\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:false,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[]},\u0022bundledWith\u0022:\u00222.5.3\u0022}",
|
||||
"sha256": "sha256:b8346ca01f40135f4a8de7ae73a601b621e228473b26516cfda65cc046d7a7c4"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"kind": "derived",
|
||||
"source": "ruby.observation",
|
||||
"locator": "document",
|
||||
"value": "{\u0022$schema\u0022:\u0022stellaops.ruby.observation@1\u0022,\u0022packages\u0022:[{\u0022name\u0022:\u0022clockwork\u0022,\u0022version\u0022:\u00223.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022ops\u0022]},{\u0022name\u0022:\u0022pagy\u0022,\u0022version\u0022:\u00226.5.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022tools\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.1.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/custom-bundle/cache/sidekiq-7.2.1.gem\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sinatra\u0022,\u0022version\u0022:\u00223.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/sinatra-3.1.0.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]}],\u0022entrypoints\u0022:[{\u0022path\u0022:\u0022config/environment.rb\u0022,\u0022type\u0022:\u0022script\u0022,\u0022requiredGems\u0022:[\u0022pagy\u0022]}],\u0022dependencyEdges\u0022:[{\u0022from\u0022:\u0022pkg:gem/pry@0.14.2\u0022,\u0022to\u0022:\u0022coderay\u0022,\u0022constraint\u0022:\u0022~\\u003E 1.1\u0022},{\u0022from\u0022:\u0022pkg:gem/pry@0.14.2\u0022,\u0022to\u0022:\u0022method_source\u0022,\u0022constraint\u0022:\u0022~\\u003E 1.0\u0022},{\u0022from\u0022:\u0022pkg:gem/sidekiq@7.2.1\u0022,\u0022to\u0022:\u0022rack\u0022,\u0022constraint\u0022:\u0022~\\u003E 2.0\u0022},{\u0022from\u0022:\u0022pkg:gem/sinatra@3.1.0\u0022,\u0022to\u0022:\u0022rack\u0022,\u0022constraint\u0022:\u0022~\\u003E 3.0\u0022}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022clockwork\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022pagy\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app/main.rb\u0022,\u0022config/environment.rb\u0022],\u0022entrypoints\u0022:[\u0022config/environment.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sinatra\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022environment\u0022:{\u0022bundlerVersion\u0022:\u00222.5.3\u0022,\u0022bundlePaths\u0022:[\u0022/mnt/e/dev/git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Ruby.Tests/bin/Debug/net10.0/Fixtures/lang/ruby/complex-app/vendor/custom-bundle\u0022],\u0022gemfiles\u0022:[\u0022/mnt/e/dev/git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Ruby.Tests/bin/Debug/net10.0/Fixtures/lang/ruby/complex-app/Gemfile\u0022],\u0022lockfiles\u0022:[\u0022Gemfile.lock\u0022],\u0022frameworks\u0022:[\u0022clockwork\u0022,\u0022sidekiq\u0022]},\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[\u0022clockwork\u0022,\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.5.3\u0022}",
|
||||
"sha256": "sha256:58c8c02011baf8711e584a4b8e33effe7292a92af69cd6eaad6c3fd869ea93e0"
|
||||
"value": "{\u0022$schema\u0022:\u0022stellaops.ruby.observation@1\u0022,\u0022packages\u0022:[{\u0022name\u0022:\u0022clockwork\u0022,\u0022version\u0022:\u00223.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022ops\u0022]},{\u0022name\u0022:\u0022pagy\u0022,\u0022version\u0022:\u00226.5.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022tools\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.1.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/custom-bundle/cache/sidekiq-7.2.1.gem\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sinatra\u0022,\u0022version\u0022:\u00223.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/sinatra-3.1.0.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]}],\u0022entrypoints\u0022:[{\u0022path\u0022:\u0022config/environment.rb\u0022,\u0022type\u0022:\u0022script\u0022,\u0022requiredGems\u0022:[\u0022pagy\u0022]}],\u0022dependencyEdges\u0022:[{\u0022from\u0022:\u0022pkg:gem/pry@0.14.2\u0022,\u0022to\u0022:\u0022coderay\u0022,\u0022constraint\u0022:\u0022~\\u003E 1.1\u0022},{\u0022from\u0022:\u0022pkg:gem/pry@0.14.2\u0022,\u0022to\u0022:\u0022method_source\u0022,\u0022constraint\u0022:\u0022~\\u003E 1.0\u0022},{\u0022from\u0022:\u0022pkg:gem/sidekiq@7.2.1\u0022,\u0022to\u0022:\u0022rack\u0022,\u0022constraint\u0022:\u0022~\\u003E 2.0\u0022},{\u0022from\u0022:\u0022pkg:gem/sinatra@3.1.0\u0022,\u0022to\u0022:\u0022rack\u0022,\u0022constraint\u0022:\u0022~\\u003E 3.0\u0022}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022clockwork\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022pagy\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app/main.rb\u0022,\u0022config/environment.rb\u0022],\u0022entrypoints\u0022:[\u0022config/environment.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sinatra\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022jobs\u0022:[{\u0022name\u0022:\u0022clockwork\u0022,\u0022type\u0022:\u0022scheduler\u0022,\u0022scheduler\u0022:\u0022clockwork\u0022},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022type\u0022:\u0022scheduler\u0022,\u0022scheduler\u0022:\u0022sidekiq\u0022}],\u0022environment\u0022:{\u0022bundlerVersion\u0022:\u00222.5.3\u0022,\u0022bundlePaths\u0022:[\u0022/mnt/e/dev/git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Ruby.Tests/bin/Debug/net10.0/Fixtures/lang/ruby/complex-app/vendor/custom-bundle\u0022],\u0022gemfiles\u0022:[\u0022/mnt/e/dev/git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Ruby.Tests/bin/Debug/net10.0/Fixtures/lang/ruby/complex-app/Gemfile\u0022],\u0022lockfiles\u0022:[\u0022Gemfile.lock\u0022],\u0022frameworks\u0022:[\u0022clockwork\u0022,\u0022sidekiq\u0022]},\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[\u0022clockwork\u0022,\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.5.3\u0022}",
|
||||
"sha256": "sha256:bd15160e034ea5adf0a8384dc9ee18557f695b0952d4fb17214f1bd1381ad22a"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3.2.0
|
||||
@@ -0,0 +1,2 @@
|
||||
ruby 3.2.0
|
||||
nodejs 20.10.0
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby "3.2.0"
|
||||
|
||||
gem "nokogiri", "~> 1.15"
|
||||
gem "pg", "~> 1.5"
|
||||
gem "puma", "~> 6.0"
|
||||
gem "rack", "~> 3.0"
|
||||
@@ -0,0 +1,29 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
mini_portile2 (2.8.4)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.0)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
pg (1.5.4)
|
||||
puma (6.4.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.7.1)
|
||||
rack (3.0.8)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
nokogiri (~> 1.15)
|
||||
pg (~> 1.5)
|
||||
puma (~> 6.0)
|
||||
rack (~> 3.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.22
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
require "rack"
|
||||
require "nokogiri"
|
||||
require "pg"
|
||||
|
||||
class App
|
||||
def call(env)
|
||||
[200, { "Content-Type" => "text/plain" }, ["Hello from container"]]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
require "rack"
|
||||
require_relative "app"
|
||||
|
||||
run App
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
workers ENV.fetch("WEB_CONCURRENCY", 2)
|
||||
threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
|
||||
threads threads_count, threads_count
|
||||
|
||||
preload_app!
|
||||
|
||||
port ENV.fetch("PORT", 3000)
|
||||
environment ENV.fetch("RACK_ENV", "development")
|
||||
@@ -0,0 +1,197 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "observation::ruby",
|
||||
"name": "Ruby Observation Summary",
|
||||
"type": "ruby-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"ruby.observation.bundler_version": "2.4.22",
|
||||
"ruby.observation.capability.exec": "false",
|
||||
"ruby.observation.capability.net": "false",
|
||||
"ruby.observation.capability.schedulers": "0",
|
||||
"ruby.observation.capability.serialization": "false",
|
||||
"ruby.observation.dependency_edges": "3",
|
||||
"ruby.observation.packages": "7",
|
||||
"ruby.observation.ruby_version": "3.2.0",
|
||||
"ruby.observation.runtime_edges": "3",
|
||||
"ruby.observation.web_server_types": "puma",
|
||||
"ruby.observation.web_servers": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "ruby.observation",
|
||||
"locator": "document",
|
||||
"value": "{\u0022$schema\u0022:\u0022stellaops.ruby.observation@1\u0022,\u0022packages\u0022:[{\u0022name\u0022:\u0022mini_portile2\u0022,\u0022version\u0022:\u00222.8.4\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022nio4r\u0022,\u0022version\u0022:\u00222.5.9\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022nokogiri\u0022,\u0022version\u0022:\u00221.15.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022pg\u0022,\u0022version\u0022:\u00221.5.4\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022puma\u0022,\u0022version\u0022:\u00226.4.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022racc\u0022,\u0022version\u0022:\u00221.7.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.0.8\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]}],\u0022entrypoints\u0022:[{\u0022path\u0022:\u0022app.rb\u0022,\u0022type\u0022:\u0022script\u0022,\u0022requiredGems\u0022:[\u0022nokogiri\u0022,\u0022pg\u0022,\u0022rack\u0022]},{\u0022path\u0022:\u0022config.ru\u0022,\u0022type\u0022:\u0022rack\u0022,\u0022requiredGems\u0022:[\u0022rack\u0022]}],\u0022dependencyEdges\u0022:[{\u0022from\u0022:\u0022pkg:gem/nokogiri@1.15.0\u0022,\u0022to\u0022:\u0022mini_portile2\u0022,\u0022constraint\u0022:\u0022~\\u003E 2.8.2\u0022},{\u0022from\u0022:\u0022pkg:gem/nokogiri@1.15.0\u0022,\u0022to\u0022:\u0022racc\u0022,\u0022constraint\u0022:\u0022~\\u003E 1.4\u0022},{\u0022from\u0022:\u0022pkg:gem/puma@6.4.0\u0022,\u0022to\u0022:\u0022nio4r\u0022,\u0022constraint\u0022:\u0022~\\u003E 2.0\u0022}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022nokogiri\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022pg\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022configs\u0022:[{\u0022name\u0022:\u0022puma\u0022,\u0022type\u0022:\u0022web-server\u0022,\u0022filePath\u0022:\u0022config/puma.rb\u0022,\u0022settings\u0022:{\u0022preload_app\u0022:\u0022true\u0022}}],\u0022environment\u0022:{\u0022rubyVersion\u0022:\u00223.2.0\u0022,\u0022bundlerVersion\u0022:\u00222.4.22\u0022,\u0022lockfiles\u0022:[\u0022Gemfile.lock\u0022],\u0022rubyVersionSources\u0022:[{\u0022version\u0022:\u00223.2.0\u0022,\u0022source\u0022:\u0022.ruby-version\u0022,\u0022sourceType\u0022:\u0022ruby-version\u0022},{\u0022version\u0022:\u00223.2.0\u0022,\u0022source\u0022:\u0022.tool-versions\u0022,\u0022sourceType\u0022:\u0022tool-versions\u0022},{\u0022version\u0022:\u00223.2.0\u0022,\u0022source\u0022:\u0022Gemfile\u0022,\u0022sourceType\u0022:\u0022gemfile\u0022}],\u0022webServers\u0022:[{\u0022serverType\u0022:\u0022puma\u0022,\u0022configPath\u0022:\u0022config/puma.rb\u0022,\u0022settings\u0022:{\u0022preload_app\u0022:\u0022true\u0022}}]},\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:false,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[]},\u0022bundledWith\u0022:\u00222.4.22\u0022}",
|
||||
"sha256": "sha256:d5c7da885e1d05805981e2080c9023cd653ed464e993d5e48de6b9f55334eca7"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/mini_portile2@2.8.4",
|
||||
"purl": "pkg:gem/mini_portile2@2.8.4",
|
||||
"name": "mini_portile2",
|
||||
"version": "2.8.4",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/nio4r@2.5.9",
|
||||
"purl": "pkg:gem/nio4r@2.5.9",
|
||||
"name": "nio4r",
|
||||
"version": "2.5.9",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/nokogiri@1.15.0",
|
||||
"purl": "pkg:gem/nokogiri@1.15.0",
|
||||
"name": "nokogiri",
|
||||
"version": "1.15.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb",
|
||||
"runtime.files": "app.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/pg@1.5.4",
|
||||
"purl": "pkg:gem/pg@1.5.4",
|
||||
"name": "pg",
|
||||
"version": "1.5.4",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb",
|
||||
"runtime.files": "app.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/puma@6.4.0",
|
||||
"purl": "pkg:gem/puma@6.4.0",
|
||||
"name": "puma",
|
||||
"version": "6.4.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/racc@1.7.1",
|
||||
"purl": "pkg:gem/racc@1.7.1",
|
||||
"name": "racc",
|
||||
"version": "1.7.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rack@3.0.8",
|
||||
"purl": "pkg:gem/rack@3.0.8",
|
||||
"name": "rack",
|
||||
"version": "3.0.8",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb;config.ru",
|
||||
"runtime.files": "app.rb;config.ru",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
ELF-fake-native-extension
|
||||
@@ -0,0 +1 @@
|
||||
ELF-fake-native-extension
|
||||
@@ -0,0 +1,14 @@
|
||||
# Rakefile for legacy app
|
||||
require 'rake'
|
||||
|
||||
desc "Run the application"
|
||||
task :run do
|
||||
ruby 'app.rb'
|
||||
end
|
||||
|
||||
desc "Run tests"
|
||||
task :test do
|
||||
sh 'ruby -Ilib test/*.rb'
|
||||
end
|
||||
|
||||
task default: :run
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env ruby
|
||||
# Old-style Ruby app without Bundler
|
||||
require 'rubygems'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
require 'yaml'
|
||||
require_relative 'lib/helper'
|
||||
|
||||
class LegacyApp
|
||||
def run
|
||||
data = load_config
|
||||
process(data)
|
||||
end
|
||||
|
||||
def load_config
|
||||
YAML.load_file('config.yml')
|
||||
end
|
||||
|
||||
def process(data)
|
||||
uri = URI.parse(data['endpoint'])
|
||||
Net::HTTP.get(uri)
|
||||
end
|
||||
end
|
||||
|
||||
LegacyApp.new.run if __FILE__ == $0
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Helper module
|
||||
require 'fileutils'
|
||||
require 'open3'
|
||||
|
||||
module Helper
|
||||
def self.execute(cmd)
|
||||
stdout, stderr, status = Open3.capture3(cmd)
|
||||
{ stdout: stdout, stderr: stderr, success: status.success? }
|
||||
end
|
||||
|
||||
def self.copy_file(src, dest)
|
||||
FileUtils.cp(src, dest)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby "3.2.0"
|
||||
|
||||
gem "rails", "~> 7.1.0"
|
||||
gem "pg", "~> 1.5"
|
||||
gem "puma", "~> 6.0"
|
||||
gem "redis", "~> 5.0"
|
||||
|
||||
group :development, :test do
|
||||
gem "rspec-rails", "~> 6.0"
|
||||
gem "factory_bot_rails", "~> 6.2"
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "rubocop-rails", "~> 2.21"
|
||||
end
|
||||
|
||||
group :production do
|
||||
gem "newrelic_rpm", "~> 9.0"
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.1.0)
|
||||
actionpack (= 7.1.0)
|
||||
actionpack (7.1.0)
|
||||
activesupport (= 7.1.0)
|
||||
rack (~> 3.0)
|
||||
activesupport (7.1.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
concurrent-ruby (1.2.2)
|
||||
factory_bot (6.2.1)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
newrelic_rpm (9.6.0)
|
||||
nio4r (2.5.9)
|
||||
pg (1.5.4)
|
||||
puma (6.4.0)
|
||||
nio4r (~> 2.0)
|
||||
rack (3.0.8)
|
||||
rails (7.1.0)
|
||||
actioncable (= 7.1.0)
|
||||
actionpack (= 7.1.0)
|
||||
activesupport (= 7.1.0)
|
||||
redis (5.0.8)
|
||||
rspec (3.12.0)
|
||||
rspec-rails (6.0.3)
|
||||
rspec (~> 3.12)
|
||||
rubocop (1.57.2)
|
||||
rubocop-rails (2.21.2)
|
||||
rubocop (~> 1.33)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
factory_bot_rails (~> 6.2)
|
||||
newrelic_rpm (~> 9.0)
|
||||
pg (~> 1.5)
|
||||
puma (~> 6.0)
|
||||
rails (~> 7.1.0)
|
||||
redis (~> 5.0)
|
||||
rspec-rails (~> 6.0)
|
||||
rubocop-rails (~> 2.21)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.22
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
require "action_controller"
|
||||
require "redis"
|
||||
require "pg"
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
before_action :authenticate_user!
|
||||
|
||||
private
|
||||
|
||||
def authenticate_user!
|
||||
# Authentication logic
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
require_relative "config/environment"
|
||||
run Rails.application
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
require "rails"
|
||||
require "action_controller/railtie"
|
||||
|
||||
module RailsApp
|
||||
class Application < Rails::Application
|
||||
config.load_defaults 7.1
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
Rails.application.routes.draw do
|
||||
root "home#index"
|
||||
|
||||
resources :users do
|
||||
member do
|
||||
post :activate
|
||||
end
|
||||
end
|
||||
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
resources :products, only: [:index, :show, :create]
|
||||
end
|
||||
end
|
||||
|
||||
get "/health", to: "health#check"
|
||||
end
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "sinatra", "~> 3.1"
|
||||
gem "sinatra-contrib", "~> 3.1"
|
||||
gem "puma", "~> 6.0"
|
||||
gem "rack", "~> 3.0"
|
||||
|
||||
group :development do
|
||||
gem "sinatra-reloader", "~> 1.0"
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
nio4r (2.5.9)
|
||||
puma (6.4.0)
|
||||
nio4r (~> 2.0)
|
||||
rack (3.0.8)
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 3.0)
|
||||
rack-session (2.0.0)
|
||||
rack (>= 3.0.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
sinatra (3.1.0)
|
||||
mustermann (~> 3.0)
|
||||
rack (~> 3.0)
|
||||
rack-protection (= 3.1.0)
|
||||
rack-session (>= 2.0.0, < 3)
|
||||
tilt (~> 2.0)
|
||||
sinatra-contrib (3.1.0)
|
||||
sinatra (= 3.1.0)
|
||||
tilt (~> 2.0)
|
||||
sinatra-reloader (1.0)
|
||||
sinatra (>= 1.4)
|
||||
tilt (2.3.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
puma (~> 6.0)
|
||||
rack (~> 3.0)
|
||||
sinatra (~> 3.1)
|
||||
sinatra-contrib (~> 3.1)
|
||||
sinatra-reloader (~> 1.0)
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.22
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
require "sinatra/base"
|
||||
require "sinatra/json"
|
||||
require "json"
|
||||
|
||||
class SinatraApp < Sinatra::Base
|
||||
get "/" do
|
||||
"Hello World"
|
||||
end
|
||||
|
||||
get "/api/users" do
|
||||
content_type :json
|
||||
{ users: [] }.to_json
|
||||
end
|
||||
|
||||
post "/api/users" do
|
||||
content_type :json
|
||||
user = JSON.parse(request.body.read)
|
||||
{ created: user }.to_json
|
||||
end
|
||||
|
||||
get "/health" do
|
||||
"OK"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
require "rack"
|
||||
require_relative "app"
|
||||
|
||||
run SinatraApp
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,246 @@
|
||||
using System.Diagnostics;
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Performance benchmarks for Ruby analyzer components.
|
||||
/// Validates determinism requirements (<100 ms / workspace, <250 MB peak memory).
|
||||
/// </summary>
|
||||
public sealed class RubyBenchmarks
|
||||
{
|
||||
private const int WarmupIterations = 3;
|
||||
private const int BenchmarkIterations = 10;
|
||||
private const int MaxAnalysisTimeMs = 100;
|
||||
|
||||
[Fact]
|
||||
public async Task SimpleApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.ElapsedMilliseconds / (double)BenchmarkIterations;
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Simple app analysis should complete in <{MaxAnalysisTimeMs}ms (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComplexApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.ElapsedMilliseconds / (double)BenchmarkIterations;
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Complex app analysis should complete in <{MaxAnalysisTimeMs}ms (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RailsApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "rails-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.ElapsedMilliseconds / (double)BenchmarkIterations;
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Rails app analysis should complete in <{MaxAnalysisTimeMs}ms (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SinatraApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "sinatra-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.ElapsedMilliseconds / (double)BenchmarkIterations;
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Sinatra app analysis should complete in <{MaxAnalysisTimeMs}ms (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContainerApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.ElapsedMilliseconds / (double)BenchmarkIterations;
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Container app analysis should complete in <{MaxAnalysisTimeMs}ms (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.ElapsedMilliseconds / (double)BenchmarkIterations;
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"Legacy app analysis should complete in <{MaxAnalysisTimeMs}ms (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CliApp_MeetsPerformanceTargetAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "cli-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
// Warmup
|
||||
for (var i = 0; i < WarmupIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Benchmark
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < BenchmarkIterations; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Assert
|
||||
var avgMs = sw.ElapsedMilliseconds / (double)BenchmarkIterations;
|
||||
avgMs.Should().BeLessThan(MaxAnalysisTimeMs, $"CLI app analysis should complete in <{MaxAnalysisTimeMs}ms (actual: {avgMs:F2}ms)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultipleRuns_ProduceDeterministicResultsAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
|
||||
var results = new List<string>();
|
||||
|
||||
// Run multiple times to verify determinism
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var context = new LanguageAnalyzerContext(fixturePath, TimeProvider.System);
|
||||
var result = await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
results.Add(result.ToJson(indent: false));
|
||||
}
|
||||
|
||||
// All results should be identical
|
||||
var firstResult = results[0];
|
||||
foreach (var result in results.Skip(1))
|
||||
{
|
||||
result.Should().Be(firstResult, "all runs should produce identical output for determinism");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,4 +100,134 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RailsWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "rails-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SinatraWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "sinatra-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContainerWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContainerWorkspaceDetectsRubyVersionAndNativeExtensionsAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "container-app");
|
||||
var store = new ScanAnalysisStore();
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
var context = new LanguageAnalyzerContext(
|
||||
fixturePath,
|
||||
TimeProvider.System,
|
||||
usageHints: null,
|
||||
services: null,
|
||||
analysisStore: store);
|
||||
|
||||
var result = await engine.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
||||
var snapshots = result.ToSnapshots();
|
||||
|
||||
var summary = Assert.Single(snapshots, snapshot => snapshot.Type == "ruby-observation");
|
||||
|
||||
// Verify Ruby version is detected
|
||||
Assert.True(summary.Metadata.TryGetValue("ruby.observation.ruby_version", out var rubyVersion));
|
||||
Assert.Equal("3.2.0", rubyVersion);
|
||||
|
||||
// Verify native extensions are detected
|
||||
Assert.True(summary.Metadata.TryGetValue("ruby.observation.native_extensions", out var nativeExtCount));
|
||||
Assert.NotNull(nativeExtCount);
|
||||
Assert.True(int.Parse(nativeExtCount!) >= 2); // nokogiri and pg
|
||||
|
||||
Assert.True(store.TryGet(ScanAnalysisKeys.RubyObservationPayload, out AnalyzerObservationPayload payload));
|
||||
using var document = JsonDocument.Parse(payload.Content.ToArray());
|
||||
var root = document.RootElement;
|
||||
var environment = root.GetProperty("environment");
|
||||
|
||||
// Check Ruby version sources
|
||||
Assert.True(environment.TryGetProperty("rubyVersionSources", out var versionSources));
|
||||
Assert.True(versionSources.GetArrayLength() >= 1);
|
||||
|
||||
// Check native extensions
|
||||
Assert.True(environment.TryGetProperty("nativeExtensions", out var nativeExtensions));
|
||||
Assert.True(nativeExtensions.GetArrayLength() >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyWorkspaceDetectsCapabilitiesWithoutBundlerAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "legacy-app");
|
||||
var store = new ScanAnalysisStore();
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
var context = new LanguageAnalyzerContext(
|
||||
fixturePath,
|
||||
TimeProvider.System,
|
||||
usageHints: null,
|
||||
services: null,
|
||||
analysisStore: store);
|
||||
|
||||
var result = await engine.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
||||
var snapshots = result.ToSnapshots();
|
||||
|
||||
var summary = Assert.Single(snapshots, snapshot => snapshot.Type == "ruby-observation");
|
||||
|
||||
// Verify capabilities are still detected from source code analysis
|
||||
Assert.True(summary.Metadata.TryGetValue("ruby.observation.capability.exec", out var usesExec));
|
||||
Assert.Equal("true", usesExec);
|
||||
|
||||
Assert.True(summary.Metadata.TryGetValue("ruby.observation.capability.net", out var usesNet));
|
||||
Assert.Equal("true", usesNet);
|
||||
|
||||
// Verify entrypoints are detected
|
||||
Assert.True(summary.Metadata.TryGetValue("ruby.observation.entrypoints", out var entrypoints));
|
||||
Assert.NotNull(entrypoints);
|
||||
Assert.True(int.Parse(entrypoints!) >= 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="8.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
|
||||
@@ -54,8 +54,8 @@ public class NativeFormatDetectorTests
|
||||
BitConverter.GetBytes((ulong)0x100).CopyTo(buffer, ph0 + 8); // p_offset
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph0 + 16); // p_vaddr
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph0 + 24); // p_paddr
|
||||
BitConverter.GetBytes((ulong)0x18).CopyTo(buffer, ph0 + 32); // p_filesz
|
||||
BitConverter.GetBytes((ulong)0x18).CopyTo(buffer, ph0 + 40); // p_memsz
|
||||
BitConverter.GetBytes((ulong)0x1C).CopyTo(buffer, ph0 + 32); // p_filesz (28 bytes for "/lib64/ld-linux-x86-64.so.2\0")
|
||||
BitConverter.GetBytes((ulong)0x1C).CopyTo(buffer, ph0 + 40); // p_memsz
|
||||
BitConverter.GetBytes((ulong)0).CopyTo(buffer, ph0 + 48); // p_align
|
||||
|
||||
// Program header 1: PT_NOTE
|
||||
|
||||
Reference in New Issue
Block a user