diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f96994ea8d..7449f90183 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -85,7 +85,7 @@ template. Your help and contribution make ScanCode docs better, we love hearing The ScanCode documentation is hosted at `scancode-toolkit.readthedocs.io `_. -If you want to contribute to Scancode Documentation, you'll find `this guide here https://scancode-toolkit.readthedocs.io/en/latest/getting-started/contribute/contributing-docs.html`_ helpful. +If you want to contribute to Scancode Documentation, you'll find `this guide here `_ helpful. Development =========== @@ -123,7 +123,7 @@ To set up ScanCode for local development: git checkout -b name-of-your-bugfix-or-feature -4. Check out the Contributing to Code Development `documentation `_, as it contains more in-depth guide for contributing code and documentation. +4. Check out the Contributing to Code Development `documentation `_, as it contains more in-depth guide for contributing code and documentation. 5. To configure your local environment for development, locate to the main directory of the local repository, and run the configure script. diff --git a/requirements.txt b/requirements.txt index 3b59481345..018df9679f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +antlr4-python3-runtime == 4.13.2 attrs==25.4.0 babel==2.17.0 banal==1.0.6 @@ -42,8 +43,10 @@ jsonstreams==0.6.0 keyring==23.7.0 license-expression==30.4.4 lxml==6.0.2 +luaparser==4.0.0 MarkupSafe==3.0.3 more-itertools==10.8.0 +multimethod==2.0.2 multiregex==2.0.3 normality==2.6.1 openpyxl==3.0.10 diff --git a/setup.cfg b/setup.cfg index 7c45f388fd..eac590e2d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -91,8 +91,9 @@ install_requires = jsonstreams >= 0.5.0 license_expression >= 30.4.4 lxml >= 5.4.0 + luaparser >= 4.0.0 MarkupSafe >= 2.1.2 - multiregex >= 2.0.3 + multiregex >= 2.0.3 normality <= 2.6.1 packageurl_python >= 0.9.0 packvers >= 21.0.0 diff --git a/src/packagedcode/__init__.py b/src/packagedcode/__init__.py index d3c48b6e25..67320a3057 100644 --- a/src/packagedcode/__init__.py +++ b/src/packagedcode/__init__.py @@ -35,6 +35,7 @@ from packagedcode import pubspec from packagedcode import pypi from packagedcode import readme +from packagedcode import rockspec from packagedcode import rpm from packagedcode import rubygems from packagedcode import swift @@ -202,6 +203,8 @@ rubygems.GemspecInExtractedGemHandler, rubygems.GemspecHandler, + rockspec.RockspecHandler, + swift.SwiftManifestJsonHandler, swift.SwiftPackageResolvedHandler, swift.SwiftShowDependenciesDepLockHandler, diff --git a/src/packagedcode/rockspec.py b/src/packagedcode/rockspec.py new file mode 100644 index 0000000000..54703141a9 --- /dev/null +++ b/src/packagedcode/rockspec.py @@ -0,0 +1,560 @@ +import os +import sys +import logging +import re +import traceback +from packageurl import PackageURL + +from luaparser import ast +from packagedcode import models + +# Debug configuration - set via environment variables +SCANCODE_DEBUG_PACKAGE = os.environ.get('SCANCODE_DEBUG_PACKAGE', False) +TRACE = SCANCODE_DEBUG_PACKAGE + + +def logger_debug(*args): + """Dummy function that does nothing by default.""" + pass + + +logger = logging.getLogger(__name__) + +# Configure logging when debug is enabled +if TRACE: + logging.basicConfig(stream=sys.stdout) + logger.setLevel(logging.DEBUG) + + def logger_debug(*args): + """Redefine to actually log debug messages.""" + return logger.debug( + ' '.join(isinstance(a, str) and a or repr(a) for a in args) + ) + + +class RockspecHandler(models.DatafileHandler): + datasource_id = 'luarocks_rockspec' + path_patterns = ('*.rockspec',) + default_package_type = 'luarocks' + default_primary_language = 'Lua' + description = 'LuaRocks rockspec file' + documentation_url = 'https://github.com/luarocks/luarocks/blob/main/docs/rockspec_format.md' + + @classmethod + def parse(cls, location, package_only=False): + """Parse rockspec file and yield PackageData object.""" + parser = RockspecParser(location) + parsed_data = parser.parse() + + # mandatory fields in rockspec files + name = parsed_data.get('package') + version = parsed_data.get('version') + vcs_url = parsed_data.get('vcs_url') + + # Extract optional fields + description = parsed_data.get('description') + homepage_url = parsed_data.get('homepage_url') + extracted_license_statement = parsed_data.get('license') + + parsed_dependencies = parsed_data.get('dependencies') or [] + + if parsed_dependencies: + dependencies = cls._build_dependent_packages(parsed_dependencies) + else: + dependencies = [] + + extra_data = cls._build_extra_data(parsed_data) + + package_data = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + name=name, + version=version, + primary_language=cls.default_primary_language, + description=description, + homepage_url=homepage_url, + vcs_url=vcs_url, + extracted_license_statement=extracted_license_statement, + dependencies=dependencies, + extra_data=extra_data, + ) + + yield models.PackageData.from_data(package_data, package_only) + + @classmethod + def _build_dependent_packages(cls, parsed_dependencies): + """Convert parsed dependency dicts to DependentPackage objects.""" + dependencies = [] + + for dep_dict in parsed_dependencies: + dep_obj = cls._create_dependent_package(dep_dict) + dependencies.append(dep_obj) + + return dependencies + + @classmethod + def _create_dependent_package(cls, dep_components): + """Create DependentPackage from parsed dependency components dict.""" + name = dep_components.get('name') + version_number = dep_components.get('version_number') + version_spec = dep_components.get('version_spec') + + purl_str = cls._create_purl_string(name, version_number) + # Determine if pinned (exact version with == operator) + is_pinned = bool(version_spec and '==' in str(version_spec)) + + return models.DependentPackage( + purl=purl_str, + extracted_requirement=version_spec, + scope='dependencies', + is_runtime=True, + is_optional=False, + is_pinned=is_pinned, + is_direct=True, + ) + + @classmethod + def _build_extra_data(cls, parsed_data): + """Extract optional rockspec metadata into extra_data dict.""" + extra_data = {} + + rockspec_format = parsed_data.get('rockspec_format') + if rockspec_format: + extra_data['rockspec_format'] = rockspec_format + + platforms = parsed_data.get('supported_platforms') + if platforms: + extra_data['supported_platforms'] = platforms + + # TODO: Extract build table fields and add to extra_data + # - build.type: the build system type (e.g., "builtin", "cmake", "make") + # - build.copy_directories: directories to include in installation + # - build.platforms: platform-specific build configurations + + return extra_data + + @classmethod + def _create_purl_string(cls, package_name, package_version): + """Return PURL string for luarocks package. Raises ValueError if package_name is empty.""" + if not package_name: + raise ValueError('Package name is required for PURL creation') + + purl = PackageURL( + type=cls.default_package_type, + name=package_name, + version=package_version + ) + return purl.to_string() + + + +class ParseError: + """Structured error representation.""" + + ERROR_MANDATORY_FIELD_MISSING = 'mandatory_field_missing' + ERROR_PARSE_FAILED = 'parse_failed' + ERROR_TABLE_EXTRACTION = 'table_extraction_failed' + + def __init__(self, error_type, field, message): + self.error_type = error_type + self.field = field + self.message = message + + def __str__(self): + return self.message + + def __repr__(self): + return f"ParseError({self.error_type}, {self.field}: {self.message})" + + +class RockspecParser: + """Parse LuaRocks rockspec files using Lua AST.""" + + def __init__(self, rockspec_path): + self.rockspec_path = rockspec_path + self.ast_tree = None + self.errors = [] + + def parse(self): + """Read file, parse AST, extract all rockspec fields and return data dict.""" + try: + code = self._read_file() + self.ast_tree = self._parse_lua(code) + + data = { + 'package': self._extract_package(), + 'version': self._extract_version(), + 'rockspec_format': self._extract_rockspec_format(), + 'supported_platforms': self._extract_supported_platforms(), + 'vcs_url': self._extract_source_url(), + 'description': self._extract_description(), + 'license': self._extract_license(), + 'homepage_url': self._extract_homepage(), + 'dependencies': self._extract_dependencies(), + } + return data + except Exception as e: + self.errors.append(ParseError(ParseError.ERROR_PARSE_FAILED, 'parse', str(e))) + traceback.print_exc() + return {} + + def _read_file(self): + """Read and return rockspec file content as string.""" + try: + with open(self.rockspec_path, 'r') as f: + return f.read() + except FileNotFoundError: + raise FileNotFoundError(f"File not found: {self.rockspec_path}") + except IOError as e: + raise IOError(f"Error reading file: {e}") + + def _parse_lua(self, code): + """Parse Lua code string into AST tree.""" + try: + return ast.parse(code) + except Exception as e: + raise RuntimeError(f"Lua parse error: {e}") + + def _find_assignment(self, var_name): + """Return (target_node, value_node) tuple for variable assignment or None.""" + if not self.ast_tree: + return None + + for node in ast.walk(self.ast_tree): + # Skip nodes that aren't assignments + if not hasattr(node, 'targets') or not hasattr(node, 'values'): # type: ignore + continue + + # Skip if no targets in this assignment + if not node.targets: # type: ignore + continue + + # Check each target to find our variable + for idx, target in enumerate(node.targets): # type: ignore + # Skip if target doesn't have an id attribute + if not hasattr(target, 'id'): + continue + + # Found the variable we're looking for + if target.id == var_name: # type: ignore + # Get the corresponding value (or first value if not enough values) + value = node.values[idx] if idx < len(node.values) else (node.values[0] if node.values else None) # type: ignore + return (target, value) + + return None + + + def _extract_string_value(self, node): + """Extract and return string value from String AST node.""" + if not node or type(node).__name__ != 'String': + return None + + s_val = node.s if hasattr(node, 's') else None + if isinstance(s_val, bytes): + return s_val.decode('utf-8') + return str(s_val) if s_val else None + + def _extract_table_values(self, table_node): + """Extract and return dict of key-value pairs from Table AST node.""" + result = {} + + if not table_node or type(table_node).__name__ != 'Table': + return result + + if not hasattr(table_node, 'fields'): + return result + + try: + for field in table_node.fields: + field_type = type(field).__name__ + + # Only process Field type nodes because they represent key-value pairs or array entries + if field_type != 'Field': + continue + + key_node = field.key if hasattr(field, 'key') else None + value_node = field.value if hasattr(field, 'value') else None + + # Skip fields without values + if not value_node: + continue + + # Extract value (works for both hash-style and array-style) + extracted_value = self._extract_value(value_node) + if extracted_value is None: + continue + + # Hash-style field: {key = value} + if key_node: + key = self._extract_key(key_node) + if key is not None: + result[key] = extracted_value + # Array-style field: {value} + else: + result[len(result)] = extracted_value + + except Exception as e: + error_msg = f'Error extracting table values: {e}' + self.errors.append(ParseError(ParseError.ERROR_TABLE_EXTRACTION, 'table', error_msg)) + + return result + + def _extract_key(self, key_node): + """Extract and return key from field key node.""" + if not key_node: + return None + + node_type = type(key_node).__name__ + + if node_type == 'String': + return self._extract_string_value(key_node) + elif node_type == 'Name' or node_type == 'Id': + return key_node.id if hasattr(key_node, 'id') else None + elif node_type == 'Number': + n_val = key_node.n if hasattr(key_node, 'n') else None + return str(n_val) if n_val is not None else None + + return None + + def _extract_value(self, node): + """Extract and return value from any AST node type.""" + if node is None: + return None + + node_type = type(node).__name__ + + # Handle each node type + if node_type == 'String': + return self._extract_string_value(node) + + elif node_type == 'Number': + number_value = node.n if hasattr(node, 'n') else None + return number_value + + elif node_type == 'Boolean': + bool_value = node.value if hasattr(node, 'value') else None + return bool_value + + elif node_type == 'Table': + return self._extract_table_values(node) + # special concat case found in the some rockspec files in the wild + elif node_type == 'Concat': + return self._extract_concat(node) + + elif node_type == 'Name': + var_name = node.id if hasattr(node, 'id') else None + if var_name: + return self._get_variable_value(var_name) + + # Unknown node type + return None + + def _get_variable_value(self, var_name): + """Look up variable by name and return its extracted value.""" + assignment = self._find_assignment(var_name) + if not assignment: + return None + + _, value = assignment + return self._extract_value(value) + + def _extract_concat(self, concat_node): + """Extract and return concatenated string from Concat AST node.""" + if not concat_node or type(concat_node).__name__ != 'Concat': + return None + + left_node = concat_node.left if hasattr(concat_node, 'left') else None + right_node = concat_node.right if hasattr(concat_node, 'right') else None + + # Recursively extract values from both sides + left_value = self._extract_value(left_node) + right_value = self._extract_value(right_node) + + # Build result from available values + has_left = left_value is not None + has_right = right_value is not None + + if has_left and has_right: + return str(left_value) + str(right_value) + elif has_left: + return str(left_value) + elif has_right: + return str(right_value) + else: + return None + + def _extract_package(self): + """Extract and return mandatory package name field.""" + assignment = self._find_assignment('package') + if not assignment: + self.errors.append(ParseError(ParseError.ERROR_MANDATORY_FIELD_MISSING, 'package', 'Missing mandatory field: package')) + return None + + _, value = assignment + result = self._extract_value(value) + return str(result) if result else None + + def _extract_version(self): + """Extract and return mandatory version field.""" + assignment = self._find_assignment('version') + if not assignment: + self.errors.append(ParseError(ParseError.ERROR_MANDATORY_FIELD_MISSING, 'version', 'Missing mandatory field: version')) + return None + + _, value = assignment + result = self._extract_value(value) + return str(result) if result else None + + def _extract_rockspec_format(self): + """Extract and return optional rockspec_format field.""" + assignment = self._find_assignment('rockspec_format') + if not assignment: + return None + + _, value = assignment + result = self._extract_value(value) + return str(result) if result else None + + def _extract_supported_platforms(self): + """Extract and return supported_platforms as sorted string list. (optional table)""" + assignment = self._find_assignment('supported_platforms') + if not assignment: + return [] + + _, platform_table_node = assignment + platform_dict = self._extract_table_values(platform_table_node) + + # Sort platforms by numeric index order + return self._sort_by_numeric_index(platform_dict) + + def _extract_source_url(self): + """Extract and return mandatory source.url field.""" + assignment = self._find_assignment('source') + if not assignment: + self.errors.append(ParseError(ParseError.ERROR_MANDATORY_FIELD_MISSING, 'source', 'Missing mandatory field: source')) + return None + + _, value = assignment + source_table = self._extract_table_values(value) + + source_url = source_table.get('url') + if not source_url: + self.errors.append(ParseError(ParseError.ERROR_MANDATORY_FIELD_MISSING, 'source.url', 'Missing mandatory field: source.url')) + return None + + return str(source_url) + + def _extract_description(self): + """Extract and return optional description.summary field.""" + assignment = self._find_assignment('description') + if not assignment: + return None + + _, value = assignment + desc_table = self._extract_table_values(value) + + summary = desc_table.get('summary') + return str(summary) if summary else None + + def _extract_license(self): + """Extract and return optional license field from description table.""" + assignment = self._find_assignment('description') + if not assignment: + return None + + _, value = assignment + desc_table = self._extract_table_values(value) + + license_val = desc_table.get('license') + return str(license_val) if license_val else None + + def _extract_homepage(self): + """Extract and return optional homepage URL from description table.""" + assignment = self._find_assignment('description') + if not assignment: + return None + + _, value = assignment + desc_table = self._extract_table_values(value) + + homepage = desc_table.get('homepage') + return str(homepage) if homepage else None + + def _extract_dependencies(self): + """Extract dependencies and return list of parsed dependency dicts.""" + assignment = self._find_assignment('dependencies') + if not assignment: + return [] + + _, dependency_table_node = assignment + dependency_strings = self._extract_table_values(dependency_table_node) + + if not dependency_strings: + return [] + + sorted_strings = self._sort_by_numeric_index(dependency_strings) + + return [ + parsed + for parsed in (self.parse_dependency(dep_string) for dep_string in sorted_strings) + if parsed is not None + ] + + def _sort_by_numeric_index(self, table_dict): + """Return values from table dict sorted by numeric keys as string list.""" + try: + # Sort by numeric key index + sorted_items = sorted( + table_dict.items(), + key=lambda x: self._numeric_key_value(x[0]) + ) + return [str(value) for _, value in sorted_items] + except Exception: + # Fallback: return values in dict order + return [str(v) for v in table_dict.values()] + + def _numeric_key_value(self, key): + """Return numeric sort key for dict key; non-numeric keys sort last.""" + if isinstance(key, int): + return key + if isinstance(key, str) and key.isdigit(): + return int(key) + return float('inf') # Non-numeric keys sort to the end + + def parse_dependency(self, dep_string): + """Parse dependency string and return dict with name, version_number, and version_spec. Returns None if parsing fails.""" + if not dep_string: + return None + + dep_string = str(dep_string).strip() + pattern = r'([a-zA-Z0-9_-]+)\s*(?:([>=<~=]+)\s*)?(.+)?' + match = re.match(pattern, dep_string) + + if not match: + return None + + name = match.group(1) + operator = match.group(2) + version_raw = match.group(3) + + version_number = None + version_spec = None + + if version_raw: + version_raw = version_raw.strip() + version_match = re.search(r'([0-9][0-9.]*)', version_raw) + if version_match: + version_number = version_match.group(1) + if operator: + version_spec = operator + ' ' + version_number + else: + version_spec = version_number + + return { + 'name': name, + 'version_number': version_number, + 'version_spec': version_spec, + } + + + diff --git a/tests/packagedcode/data/plugin/plugins_list_linux.txt b/tests/packagedcode/data/plugin/plugins_list_linux.txt index eb4763d6c7..a8e9ad0de7 100755 --- a/tests/packagedcode/data/plugin/plugins_list_linux.txt +++ b/tests/packagedcode/data/plugin/plugins_list_linux.txt @@ -545,6 +545,13 @@ Package type: linux-distro description: Linux OS release metadata file path_patterns: '*etc/os-release', '*usr/lib/os-release' -------------------------------------------- +Package type: luarocks + datasource_id: luarocks_rockspec + documentation URL: https://github.com/luarocks/luarocks/blob/main/docs/rockspec_format.md + primary language: Lua + description: LuaRocks rockspec file + path_patterns: '*.rockspec' +-------------------------------------------- Package type: maven datasource_id: build_gradle documentation URL: None diff --git a/tests/packagedcode/data/rockspec/test.rockspec b/tests/packagedcode/data/rockspec/test.rockspec new file mode 100644 index 0000000000..6e1f4d0d17 --- /dev/null +++ b/tests/packagedcode/data/rockspec/test.rockspec @@ -0,0 +1,30 @@ +package = "lua-cjson" +version = "2.1.0.16-1" + +source = { + url = "git+https://github.com/openresty/lua-cjson", + tag = "2.1.0.16", +} + +description = { + summary = "A fast JSON encoding/parsing module", + detailed = [[ + The Lua CJSON module provides JSON support for Lua. It features: + - Fast, standards compliant encoding/parsing routines + - Full support for JSON with UTF-8, including decoding surrogate pairs + - Optional run-time support for common exceptions to the JSON specification + (infinity, NaN,..) + - No dependencies on other libraries + ]], + homepage = "http://www.kyne.com.au/~mark/software/lua-cjson.php", + license = "MIT" +} + +dependencies = { + "lua >= 5.1" +} + +build = { + type = "builtin", + copy_directories = { "tests" } +} \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test.rockspec-expected.json b/tests/packagedcode/data/rockspec/test.rockspec-expected.json new file mode 100644 index 0000000000..c3e476b1df --- /dev/null +++ b/tests/packagedcode/data/rockspec/test.rockspec-expected.json @@ -0,0 +1,81 @@ +[ + { + "type": "luarocks", + "namespace": null, + "name": "lua-cjson", + "version": "2.1.0.16-1", + "qualifiers": {}, + "subpath": null, + "primary_language": "Lua", + "description": "A fast JSON encoding/parsing module", + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": "http://www.kyne.com.au/~mark/software/lua-cjson.php", + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "git+https://github.com/openresty/lua-cjson", + "copyright": null, + "holder": null, + "declared_license_expression": "mit", + "declared_license_expression_spdx": "MIT", + "license_detections": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "matches": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "from_file": null, + "start_line": 1, + "end_line": 1, + "matcher": "1-spdx-id", + "score": 100.0, + "matched_length": 1, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06", + "rule_url": null, + "matched_text": "MIT" + } + ], + "identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "MIT", + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": {}, + "dependencies": [ + { + "purl": "pkg:luarocks/lua@5.1", + "extracted_requirement": ">= 5.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "luarocks_rockspec", + "purl": "pkg:luarocks/lua-cjson@2.1.0.16-1" + } +] \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test1.rockspec b/tests/packagedcode/data/rockspec/test1.rockspec new file mode 100644 index 0000000000..ed376c86b2 --- /dev/null +++ b/tests/packagedcode/data/rockspec/test1.rockspec @@ -0,0 +1,48 @@ +package = "kong" +version = "3.3.0-0" +rockspec_format = "3.0" +supported_platforms = {"linux", "macosx"} +source = { + url = "git+https://github.com/Kong/kong.git", + tag = "3.3.0" +} +description = { + summary = "Kong is a scalable and customizable API Management Layer built on top of Nginx.", + homepage = "https://konghq.com", + license = "Apache 2.0" +} +dependencies = { + "inspect == 3.1.3", + "luasec == 1.3.1", + "luasocket == 3.0-rc1", + "penlight == 1.13.1", + "lua-resty-http == 0.17.1", + "lua-resty-jit-uuid == 0.0.7", + "lua-ffi-zlib == 0.5", + "multipart == 0.5.9", + "version == 1.0.1", + "kong-lapis == 1.8.3.1", + "lua-cassandra == 1.5.2", + "pgmoon == 1.16.0", + "luatz == 0.4", + "lua_system_constants == 0.1.4", + "lyaml == 6.2.8", + "luasyslog == 2.0.1", + "lua_pack == 2.0.0", + "binaryheap >= 0.4", + "luaxxhash >= 1.0", + "lua-protobuf == 0.5.0", + "lua-resty-healthcheck == 1.6.2", + "lua-resty-mlcache == 2.6.0", + "lua-messagepack == 0.5.2", + "lua-resty-openssl == 0.8.20", + "lua-resty-counter == 0.2.1", + "lua-resty-ipmatcher == 0.6.1", + "lua-resty-acme == 0.11.0", + "lua-resty-session == 4.0.3", + "lua-resty-timer-ng == 0.2.5", + "lpeg == 1.0.2", +} +build = { + type = "builtin" +} diff --git a/tests/packagedcode/data/rockspec/test1.rockspec-expected.json b/tests/packagedcode/data/rockspec/test1.rockspec-expected.json new file mode 100644 index 0000000000..6e9f5d5540 --- /dev/null +++ b/tests/packagedcode/data/rockspec/test1.rockspec-expected.json @@ -0,0 +1,406 @@ +[ + { + "type": "luarocks", + "namespace": null, + "name": "kong", + "version": "3.3.0-0", + "qualifiers": {}, + "subpath": null, + "primary_language": "Lua", + "description": "Kong is a scalable and customizable API Management Layer built on top of Nginx.", + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": "https://konghq.com", + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "git+https://github.com/Kong/kong.git", + "copyright": null, + "holder": null, + "declared_license_expression": "apache-2.0", + "declared_license_expression_spdx": "Apache-2.0", + "license_detections": [ + { + "license_expression": "apache-2.0", + "license_expression_spdx": "Apache-2.0", + "matches": [ + { + "license_expression": "apache-2.0", + "license_expression_spdx": "Apache-2.0", + "from_file": null, + "start_line": 1, + "end_line": 1, + "matcher": "1-hash", + "score": 100.0, + "matched_length": 3, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx_license_id_apache-2.0_for_apache-2.0.RULE", + "rule_url": "https://github.com/nexB/scancode-toolkit/tree/develop/src/licensedcode/data/rules/spdx_license_id_apache-2.0_for_apache-2.0.RULE", + "matched_text": "Apache 2.0" + } + ], + "identifier": "apache_2_0-d66ab77d-a5cc-7104-e702-dc7df61fe9e8" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "Apache 2.0", + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "rockspec_format": "3.0", + "supported_platforms": [ + "linux", + "macosx" + ] + }, + "dependencies": [ + { + "purl": "pkg:luarocks/inspect@3.1.3", + "extracted_requirement": "== 3.1.3", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/luasec@1.3.1", + "extracted_requirement": "== 1.3.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/luasocket@3.0", + "extracted_requirement": "== 3.0", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/penlight@1.13.1", + "extracted_requirement": "== 1.13.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-http@0.17.1", + "extracted_requirement": "== 0.17.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-jit-uuid@0.0.7", + "extracted_requirement": "== 0.0.7", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-ffi-zlib@0.5", + "extracted_requirement": "== 0.5", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/multipart@0.5.9", + "extracted_requirement": "== 0.5.9", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/version@1.0.1", + "extracted_requirement": "== 1.0.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/kong-lapis@1.8.3.1", + "extracted_requirement": "== 1.8.3.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-cassandra@1.5.2", + "extracted_requirement": "== 1.5.2", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/pgmoon@1.16.0", + "extracted_requirement": "== 1.16.0", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/luatz@0.4", + "extracted_requirement": "== 0.4", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua_system_constants@0.1.4", + "extracted_requirement": "== 0.1.4", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lyaml@6.2.8", + "extracted_requirement": "== 6.2.8", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/luasyslog@2.0.1", + "extracted_requirement": "== 2.0.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua_pack@2.0.0", + "extracted_requirement": "== 2.0.0", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/binaryheap@0.4", + "extracted_requirement": ">= 0.4", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/luaxxhash@1.0", + "extracted_requirement": ">= 1.0", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-protobuf@0.5.0", + "extracted_requirement": "== 0.5.0", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-healthcheck@1.6.2", + "extracted_requirement": "== 1.6.2", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-mlcache@2.6.0", + "extracted_requirement": "== 2.6.0", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-messagepack@0.5.2", + "extracted_requirement": "== 0.5.2", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-openssl@0.8.20", + "extracted_requirement": "== 0.8.20", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-counter@0.2.1", + "extracted_requirement": "== 0.2.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-ipmatcher@0.6.1", + "extracted_requirement": "== 0.6.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-acme@0.11.0", + "extracted_requirement": "== 0.11.0", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-session@4.0.3", + "extracted_requirement": "== 4.0.3", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lua-resty-timer-ng@0.2.5", + "extracted_requirement": "== 0.2.5", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:luarocks/lpeg@1.0.2", + "extracted_requirement": "== 1.0.2", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "luarocks_rockspec", + "purl": "pkg:luarocks/kong@3.3.0-0" + } +] \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test2.rockspec b/tests/packagedcode/data/rockspec/test2.rockspec new file mode 100644 index 0000000000..8bf134e401 --- /dev/null +++ b/tests/packagedcode/data/rockspec/test2.rockspec @@ -0,0 +1,29 @@ +package = "LuaSocket" +version = "scm-3" +source = { + url = "git+https://github.com/lunarmodules/luasocket.git", + branch = "master" +} +description = { + summary = "Network support for the Lua language", + detailed = [[ + LuaSocket is a Lua extension library composed of two parts: a set of C + modules that provide support for the TCP and UDP transport layers, and a + set of Lua modules that provide functions commonly needed by applications + that deal with the Internet. + ]], + homepage = "https://github.com/lunarmodules/luasocket", + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} + +build = { + type = "builtin", + copy_directories = { + "docs", + "samples", + "test" + } +} \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test2.rockspec-expected.json b/tests/packagedcode/data/rockspec/test2.rockspec-expected.json new file mode 100644 index 0000000000..6c12f8dde8 --- /dev/null +++ b/tests/packagedcode/data/rockspec/test2.rockspec-expected.json @@ -0,0 +1,81 @@ +[ + { + "type": "luarocks", + "namespace": null, + "name": "LuaSocket", + "version": "scm-3", + "qualifiers": {}, + "subpath": null, + "primary_language": "Lua", + "description": "Network support for the Lua language", + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": "https://github.com/lunarmodules/luasocket", + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "git+https://github.com/lunarmodules/luasocket.git", + "copyright": null, + "holder": null, + "declared_license_expression": "mit", + "declared_license_expression_spdx": "MIT", + "license_detections": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "matches": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "from_file": null, + "start_line": 1, + "end_line": 1, + "matcher": "1-spdx-id", + "score": 100.0, + "matched_length": 1, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06", + "rule_url": null, + "matched_text": "MIT" + } + ], + "identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "MIT", + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": {}, + "dependencies": [ + { + "purl": "pkg:luarocks/lua@5.1", + "extracted_requirement": ">= 5.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "luarocks_rockspec", + "purl": "pkg:luarocks/luasocket@scm-3" + } +] \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test3.rockspec b/tests/packagedcode/data/rockspec/test3.rockspec new file mode 100644 index 0000000000..b7e4a9984f --- /dev/null +++ b/tests/packagedcode/data/rockspec/test3.rockspec @@ -0,0 +1,29 @@ +rockspec_format = "3.0" +package = "vdsl" +version = "0.1.0-1" + +source = { + url = "git+https://github.com/ynishi/vdsl.git", + tag = "v0.1.0", +} + +description = { + summary = "Visual DSL for ComfyUI", + detailed = [[ + vdsl transforms semantic scene composition into ComfyUI node graphs. + Pure Lua. Zero dependencies. + Images become portable project files through PNG-embedded recipes. + ]], + homepage = "https://github.com/ynishi/vdsl", + license = "MIT", + labels = { "comfyui", "dsl", "image-generation", "stable-diffusion" }, +} + +dependencies = { + "lua >= 5.1", +} + +build = { + type = "builtin", + copy_directories = { "examples", "tests" }, +} \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test3.rockspec-expected.json b/tests/packagedcode/data/rockspec/test3.rockspec-expected.json new file mode 100644 index 0000000000..8b67e87eea --- /dev/null +++ b/tests/packagedcode/data/rockspec/test3.rockspec-expected.json @@ -0,0 +1,83 @@ +[ + { + "type": "luarocks", + "namespace": null, + "name": "vdsl", + "version": "0.1.0-1", + "qualifiers": {}, + "subpath": null, + "primary_language": "Lua", + "description": "Visual DSL for ComfyUI", + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": "https://github.com/ynishi/vdsl", + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "git+https://github.com/ynishi/vdsl.git", + "copyright": null, + "holder": null, + "declared_license_expression": "mit", + "declared_license_expression_spdx": "MIT", + "license_detections": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "matches": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "from_file": null, + "start_line": 1, + "end_line": 1, + "matcher": "1-spdx-id", + "score": 100.0, + "matched_length": 1, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06", + "rule_url": null, + "matched_text": "MIT" + } + ], + "identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "MIT", + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "rockspec_format": "3.0" + }, + "dependencies": [ + { + "purl": "pkg:luarocks/lua@5.1", + "extracted_requirement": ">= 5.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "luarocks_rockspec", + "purl": "pkg:luarocks/vdsl@0.1.0-1" + } +] \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test4.rockspec b/tests/packagedcode/data/rockspec/test4.rockspec new file mode 100644 index 0000000000..f49f6f9095 --- /dev/null +++ b/tests/packagedcode/data/rockspec/test4.rockspec @@ -0,0 +1,32 @@ +---@diagnostic disable: lowercase-global + +local _MODREV, _SPECREV = "scm", "-1" +rockspec_format = "3.0" +version = _MODREV .. _SPECREV + +local user = "S1M0N38" +package = "claude.nvim" + +description = { + summary = "A simple plugin to integrate Claude Code in Neovim", + detailed = [[ +claude.nvim is a simple plugin to integrate Claude Code in Neovim. + ]], + labels = { "neovim", "plugin", "lua", "claude", "ai" }, + homepage = "https://github.com/" .. user .. "/" .. package, + license = "MIT", +} + +dependencies = { + "lua >= 5.1", +} + + +source = { + url = "git://github.com/" .. user .. "/" .. package, +} + +build = { + type = "builtin", + copy_directories = { "plugin", "doc", "scripts" }, +} \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test4.rockspec-expected.json b/tests/packagedcode/data/rockspec/test4.rockspec-expected.json new file mode 100644 index 0000000000..aa0b6af26d --- /dev/null +++ b/tests/packagedcode/data/rockspec/test4.rockspec-expected.json @@ -0,0 +1,83 @@ +[ + { + "type": "luarocks", + "namespace": null, + "name": "claude.nvim", + "version": "scm-1", + "qualifiers": {}, + "subpath": null, + "primary_language": "Lua", + "description": "A simple plugin to integrate Claude Code in Neovim", + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": "https://github.com/S1M0N38/claude.nvim", + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "git://github.com/S1M0N38/claude.nvim", + "copyright": null, + "holder": null, + "declared_license_expression": "mit", + "declared_license_expression_spdx": "MIT", + "license_detections": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "matches": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "from_file": null, + "start_line": 1, + "end_line": 1, + "matcher": "1-spdx-id", + "score": 100.0, + "matched_length": 1, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06", + "rule_url": null, + "matched_text": "MIT" + } + ], + "identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "MIT", + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "rockspec_format": "3.0" + }, + "dependencies": [ + { + "purl": "pkg:luarocks/lua@5.1", + "extracted_requirement": ">= 5.1", + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "luarocks_rockspec", + "purl": "pkg:luarocks/claude.nvim@scm-1" + } +] \ No newline at end of file diff --git a/tests/packagedcode/data/rockspec/test_file_source.txt b/tests/packagedcode/data/rockspec/test_file_source.txt new file mode 100644 index 0000000000..1e44909ebe --- /dev/null +++ b/tests/packagedcode/data/rockspec/test_file_source.txt @@ -0,0 +1,11 @@ +Test File : Source + +1. test.rockspec : https://github.com/openresty/lua-cjson/blob/master/lua-cjson-2.1.0.16-1.rockspec + +2. test1.rockspec : https://github.com/Kong/kong/blob/master/kong-latest.rockspec + +3. test2.rockspec : https://github.com/lunarmodules/luasocket/blob/master/luasocket-scm-3.rockspec + +4. test3.rockspec : https://github.com/ynishi/vdsl/blob/main/vdsl-0.1.0-1.rockspec + +5. test4.rockspec : https://github.com/S1M0N38/claude.nvim/blob/main/claude.nvim-scm-1.rockspec diff --git a/tests/packagedcode/test_rockspec.py b/tests/packagedcode/test_rockspec.py new file mode 100644 index 0000000000..5b74bc49e8 --- /dev/null +++ b/tests/packagedcode/test_rockspec.py @@ -0,0 +1,105 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# ScanCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/nexB/scancode-toolkit for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. + + +import os +import tempfile + +from packagedcode import rockspec +from packages_test_utils import PackageTester +from scancode_config import REGEN_TEST_FIXTURES + + +class TestRockspecHandler(PackageTester): + """ + Tests for RockspecHandler following ScanCode's testing patterns. + + Tests use the comprehensive JSON snapshot approach with check_packages_data() + to compare entire handler output against expected JSON files. This provides + better visibility into the complete output and makes it easier to detect + any changes. + """ + + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_parse_test_rockspec(self): + """Test parsing test.rockspec.""" + test_file = self.get_test_loc('rockspec/test.rockspec') + packages = rockspec.RockspecHandler.parse(test_file) + expected_loc = self.get_test_loc('rockspec/test.rockspec-expected.json') + self.check_packages_data(packages, expected_loc, regen=REGEN_TEST_FIXTURES) + + def test_parse_test1_rockspec(self): + """Test parsing test1.rockspec with mandatory and optional fields.""" + test_file = self.get_test_loc('rockspec/test1.rockspec') + packages = rockspec.RockspecHandler.parse(test_file) + expected_loc = self.get_test_loc('rockspec/test1.rockspec-expected.json') + self.check_packages_data(packages, expected_loc, regen=REGEN_TEST_FIXTURES) + + def test_parse_test2_rockspec(self): + """Test parsing test2.rockspec.""" + test_file = self.get_test_loc('rockspec/test2.rockspec') + packages = rockspec.RockspecHandler.parse(test_file) + expected_loc = self.get_test_loc('rockspec/test2.rockspec-expected.json') + self.check_packages_data(packages, expected_loc, regen=REGEN_TEST_FIXTURES) + + def test_parse_test3_rockspec(self): + """Test parsing test3.rockspec.""" + test_file = self.get_test_loc('rockspec/test3.rockspec') + packages = rockspec.RockspecHandler.parse(test_file) + expected_loc = self.get_test_loc('rockspec/test3.rockspec-expected.json') + self.check_packages_data(packages, expected_loc, regen=REGEN_TEST_FIXTURES) + + def test_parse_test4_rockspec(self): + """Test parsing test4.rockspec with variable concatenation.""" + test_file = self.get_test_loc('rockspec/test4.rockspec') + packages = rockspec.RockspecHandler.parse(test_file) + expected_loc = self.get_test_loc('rockspec/test4.rockspec-expected.json') + self.check_packages_data(packages, expected_loc, regen=REGEN_TEST_FIXTURES) + + def test_handler_is_registered(self): + """Test that RockspecHandler is registered in the application handlers.""" + from packagedcode import APPLICATION_PACKAGE_DATAFILE_HANDLERS + handlers = [h for h in APPLICATION_PACKAGE_DATAFILE_HANDLERS + if h.datasource_id == 'luarocks_rockspec'] + assert len(handlers) == 1, f"Expected 1 RockspecHandler, found {len(handlers)}" + assert handlers[0] == rockspec.RockspecHandler + + def test_handler_in_datasource_registry(self): + """Test that handler is registered in HANDLER_BY_DATASOURCE_ID registry.""" + from packagedcode import HANDLER_BY_DATASOURCE_ID + handler = HANDLER_BY_DATASOURCE_ID.get('luarocks_rockspec') + assert handler is not None, "RockspecHandler not found in registry" + assert handler == rockspec.RockspecHandler + + def test_handler_attributes(self): + """Test that handler has required attributes.""" + assert rockspec.RockspecHandler.datasource_id == 'luarocks_rockspec' + assert rockspec.RockspecHandler.path_patterns == ('*.rockspec',) + assert rockspec.RockspecHandler.default_package_type == 'luarocks' + assert rockspec.RockspecHandler.default_primary_language == 'Lua' + assert rockspec.RockspecHandler.description is not None + + def test_is_datafile_rockspec(self): + """Test that is_datafile recognizes .rockspec files.""" + test_file = self.get_test_loc('rockspec/test1.rockspec') + assert rockspec.RockspecHandler.is_datafile(test_file) + + def test_is_datafile_non_rockspec(self): + """Test that is_datafile rejects non-.rockspec files.""" + with tempfile.NamedTemporaryFile(suffix='.txt', delete=False) as f: + temp_file = f.name + try: + assert not rockspec.RockspecHandler.is_datafile(temp_file) + finally: + os.unlink(temp_file) + + +if __name__ == '__main__': + import pytest + pytest.main([__file__, '-v'])