diff --git a/.reuse/dep5 b/.reuse/dep5 index b110a4c..ec76bbb 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,8 +1,11 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: licomp_osadl +Upstream-Name: licomp_toolkit Upstream-Contact: Henrik Sandklef -Source: https://github.com/hesa/licomp_osadl +Source: https://github.com/hesa/licomp_toolkit +Files: licomp_toolkit/data/reply_schema.json +Copyright: Henrik Sandklef +License: GPL-3.0-or-later Files: .github/** Copyright: Henrik Sandklef diff --git a/devel/licomp-all.sh b/devel/licomp-all.sh new file mode 100755 index 0000000..8913567 --- /dev/null +++ b/devel/licomp-all.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + + + +RESOURCES=$(./devel/licomp-toolkit supported-resources | jq -r .[] | cut -d : -f 1 | sed 's,_,-,g') + +for RESOURCE in $RESOURCES +do + echo "# $RESOURCE" + $RESOURCE $* +done diff --git a/devel/licomp-toolkit b/devel/licomp-toolkit index da20856..c9ad5d1 100755 --- a/devel/licomp-toolkit +++ b/devel/licomp-toolkit @@ -4,14 +4,16 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})/../ + if [ "$1" = "--local" ] then shift - EXTRA_PYTHONPATH=:../licomp:../licomp-osadl:../licomp-reclicense:../licomp-proprietary:../licomp-hermione:../licomp-dwheeler + EXTRA_PYTHONPATH=:${SCRIPT_DIR}/../licomp:${SCRIPT_DIR}/../licomp-osadl:${SCRIPT_DIR}/../licomp-reclicense:${SCRIPT_DIR}/../licomp-proprietary:${SCRIPT_DIR}/../licomp-hermione:${SCRIPT_DIR}/../licomp-dwheeler fi if [ "$1" = "" ] then - ARGS="verify -il MIT -ol MIT" + ARGS="verify -il MIT -ol \"MIT OR X11\"" fi -PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ./licomp_toolkit/__main__.py $* $ARGS +PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index ffe243c..be16595 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -10,16 +10,18 @@ from licomp.interface import LicompException from licomp_toolkit.toolkit import LicompToolkit -from licomp_toolkit.toolkit import LicompToolkitFormatter +from licomp_toolkit.toolkit import ExpressionExpressionChecker +from licomp_toolkit.format import LicompToolkitFormatter from licomp_toolkit.config import cli_name from licomp_toolkit.config import description from licomp_toolkit.config import epilog -from licomp_toolkit.utils import licomp_results_to_return_code +from licomp_toolkit.schema_checker import LicompToolkitSchemaChecker from licomp.main_base import LicompParser from licomp.interface import UseCase from licomp.interface import Provisioning from licomp.return_codes import ReturnCodes +from licomp.return_codes import compatibility_status_to_returncode from flame.license_db import FossLicenses from flame.exception import FlameException @@ -34,14 +36,21 @@ def __init__(self, name, description, epilog, default_usecase, default_provision def __normalize_license(self, lic_name): return self.flame.expression_license(lic_name, update_dual=False)['identified_license'] + def validate(self, args): + LicompToolkitSchemaChecker().validate_file(args.file_name, deep=True) + return None, ReturnCodes.LICOMP_OK.value, None + def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: - compatibilities = self.licomp_toolkit.outbound_inbound_compatibility(self.__normalize_license(args.out_license), - self.__normalize_license(args.in_license), - args.usecase, - args.provisioning) - ret_code = licomp_results_to_return_code(compatibilities['summary']['results']) + expr_checker = ExpressionExpressionChecker() + compatibilities = expr_checker.check_compatibility(self.__normalize_license(args.out_license), + self.__normalize_license(args.in_license), + args.usecase, + args.provisioning, + detailed_report=True) + + ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) return formatter.format_compatibilities(compatibilities), ret_code, False except LicompException as e: return e, e.return_code.value, True @@ -50,7 +59,8 @@ def verify(self, args): def supported_licenses(self, args): licenses = self.licomp_toolkit.supported_licenses() - return licenses, ReturnCodes.LICOMP_OK.value, None + formatter = LicompToolkitFormatter.formatter(args.output_format) + return formatter.format_licomp_resources(licenses), ReturnCodes.LICOMP_OK.value, None def supported_usecases(self, args): usecases = self.licomp_toolkit.supported_usecases() @@ -65,7 +75,8 @@ def supported_provisionings(self, args): return provisioning_names, ReturnCodes.LICOMP_OK.value, None def supported_resources(self, args): - return [f'{x.name()}:{x.version()}' for x in self.licomp_toolkit.licomp_resources().values()], ReturnCodes.LICOMP_OK, False + formatter = LicompToolkitFormatter.formatter(args.output_format) + return formatter.format_licomp_resources([f'{x.name()}:{x.version()}' for x in self.licomp_toolkit.licomp_resources().values()]), ReturnCodes.LICOMP_OK.value, False def supports_license(self, args): lic = args.license @@ -131,7 +142,8 @@ def main(): res, code, err, func = lct_parser.run_noexit() if _working_return_code(code): - print(res) + if res: + print(res) else: print(res, file=sys.stderr) diff --git a/licomp_toolkit/config.py b/licomp_toolkit/config.py index c605e45..69746df 100644 --- a/licomp_toolkit/config.py +++ b/licomp_toolkit/config.py @@ -8,7 +8,7 @@ cli_name = 'licomp-toolkit' module_name = 'licomp_toolkit' -disclaimer = 'This software and the data come with no gurantee. For more information read the disclaimers from the individual compatibility resources, and contact a lawyer to make sure your software is compliant.' +disclaimer = 'This software and the data come with no guarantee. For more information read the disclaimers from the individual compatibility resources, and contact a lawyer to make sure your software is compliant.' description = """ Simple command line tool to check compatibility between two licenses, given context diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json new file mode 100644 index 0000000..c64bc04 --- /dev/null +++ b/licomp_toolkit/data/reply_schema.json @@ -0,0 +1,255 @@ +{ + "$schema" : "http://json-schema.org/draft-07/schema#", + "$id" : "", + "title" : "Licomp Toolkit Reply", + "type" : "object", + "properties" : { + "compatibility_report": { + "type" : "object", + "$ref": "#/$defs/compatibility_object" + }, + "outbound" : { + "$ref": "#/$defs/license", + "description" : "The outbound license expression." + }, + "inbound" : { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "usecase" : { + "type" : "string", + "enum": ["library", "compiler", "snippet", "tool", "test"], + "description" : "Usecase for the compatibility check, e.g. library (as in using the inbound licensed component as a library)" + }, + "provisioning" : { + "type" : "string", + "enum": ["source-code-distribution", "binary-distribution", "local-use", "provide-service", "provide-webui"], + "description" : "The way the component is provided to the user for the compatibility check, e.g. binary-distribution" + }, + "modification" : { + "type" : "string", + "description" : "Has the component been modified. Currently not used/implemented.", + "enum": [ "unmodified", "modified"] + }, + "compatibility" : { + "$ref": "#/$defs/compatibility", + "description" : "The inbound license expression." + } + }, + "required" : [ "compatibility_report", "compatibility", "outbound", "inbound" , "usecase" ], + "additionalProperties" : false, + "$defs": { + "compatibility_object": { + "type" : "object", + "oneOf": [ + { + "properties": { + "compatibility": { + "$ref": "#/$defs/compatibility", + "description" : "The inbound license expression." + }, + "compatibility_type": { + "enum": [ "license" ], + "description": "Describing what is being checked. Can be either expression or license. In this case the value is \"license\"." + }, + "compatibility_check": { + "$ref": "#/$defs/compatibility_check" + }, + "inbound_license": { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "outbound_license": { + "$ref": "#/$defs/license", + "description" : "The outbound license expression." + }, + "compatibility_details": { + "oneOf": [ + { + "type": "object", + "properties": { + "compatibilities": { + "type": "array", + "items": { + "$ref": "#/$defs/licomp_object" + } + } + } + }, + { + "type": "null" + } + ] + }, + "compatibility_object": { + "type" : "object", + "anyOf": [ + { + "$ref": "#/$defs/compatibility_object" + }, + { + "properties": {} + } + ] + } + }, + "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object", "compatibility_details"], + "additionalProperties" : false + }, + { + "properties": { + "compatibility" : { + "$ref": "#/$defs/compatibility", + "description" : "The inbound license expression." + }, + "compatibility_type": { + "enum": [ "expression" ], + "description": "Describing what is being checked. Can be either expression or license. In this case the value is \"expression\"." + }, + "compatibility_check": { + "$ref": "#/$defs/compatibility_check" + }, + "inbound_license": { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "outbound_license": { + "$ref": "#/$defs/license", + "description" : "The inbound license expression." + }, + "compatibility_details": { + "type": "null" + }, + "operator": { + "enum": [ "AND", "OR" ], + "description" : "The logical operator between two license expressions." + }, + "operands": { + "type": "array", + "items": { + "compatibility_object": { + "type" : "object", + "$ref": "#/$defs/compatibility_object" + }, + "compatibility": { "type": "string" } + }, + "description": "The operands for the operator. The operands can be either a license or an operator." + } + }, + "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license" , "operator", "operands" ], + "additionalProperties" : false + } + ] + }, + "compatibility" : { + "type" : "string", + "enum": [ "yes", "no", "depends", "unknown", "unsupported", "mixed", null], + "description" : "The compatbility between the Outbound and Inbound license expressions" + }, + "license" : { + "type" : "string", + "minLength": 1 + }, + "compatibility_check": { + "type": "string", + "enum": [ + "outbound-expression -> inbound-expression", + "outbound-expression -> inbound-license", + "outbound-license -> inbound-expression", + "outbound-license -> inbound-license" + ], + "description" : "A text describing if outbound license or expression is checked for compatiblility against inbound license or expression." + }, + "licomp_object": { + "type" : "object", + "properties" : { + "status" : { + "enum": [ "failure", "success" ], + "description" : "Overall status indicating if the compatibility check succeeded." + }, + "status_details" : { + "type" : "object", + "properties" : { + "provisioning_status": { + "enum": [ "failure", "success" ], + "description" : "Status indicating if the provisioning is supported." + }, + "usecase_status": { + "enum": [ "failure", "success" ], + "description" : "Status indicating if the usecase is supported." + }, + "license_supported_status": { + "enum": [ "failure", "success" ], + "description" : "Status indicating if the licenses are supported." + } + } + }, + "outbound" : { + "type" : "string", + "minLength": 1, + "description" : "The outbound license" + }, + "inbound" : { + "type" : "string", + "minLength": 1, + "description" : "The inbound license" + }, + "usecase" : { + "type" : "string", + "enum": ["library", "compiler", "snippet", "tool", "test"], + "description" : "Usecase for the compatibility check, e.g. library (as in using the inbound licensed component as a library)" + }, + "provisioning" : { + "type" : "string", + "enum": ["source-code-distribution", "binary-distribution", "local-use", "provide-service", "provide-webui"], + "description" : "The way the component is provided to the user for the compatibility check, e.g. binary-distribution" + }, + "modification" : { + "type" : "string", + "description" : "Has the component been modified. Currently not used/implemented.", + "enum": [ "unmodified", "modified"] + }, + "compatibility_status" : { + "enum": [ "yes", "no", "depends", "unknown", "unsupported", null], + "description" : "The compatbility between the Outbound and Inbound license expressions" + }, + "explanation" : { + "type" : [ "string", "null" ], + "description" : "A text describing the compatiblity, e.g. how the compatibility was determined" + }, + "api_version" : { + "type" : "string", + "pattern": "^[0-9].[0-9](.[0-9]){0,1}$", + "description" : "The api version of the program providing the reply" + }, + "resource_name1" : { + "type" : "string", + "minLength": 1, + "description" : "The name of the program providing the reply, e.g. licomp-osadl" + }, + "resource_version" : { + "type" : "string", + "pattern": "^[0-9].[0-9](.[0-9]){0,1}$", + "description" : "The versions of the program providing the reply, e.g. licomp-osadl" + }, + "resource_disclaimer" : { + "type" : "string", + "minLength": 10, + "description" : "A disclaimer of the program providing the reply." + }, + "data_url" : { + "type" : "string", + "minLength": 10, + "description" : "A URL pointing to the data used to provide the resulting compatibility." + }, + "resource_url" : { + "type" : "string", + "minLength": 10, + "description" : "A URL pointing to the project page (or similar) for the program providing the resulting compatibility." + } + }, + "required" : [ "status", "status_details", "outbound", "inbound", "usecase", "provisioning", "modification", "compatibility_status", "explanation", "api_version", "resource_name1", "resource_version", "resource_disclaimer", "data_url", "resource_url"], + "additionalProperties" : false + } + } +} diff --git a/licomp_toolkit/expr_parser.py b/licomp_toolkit/expr_parser.py new file mode 100644 index 0000000..df09af5 --- /dev/null +++ b/licomp_toolkit/expr_parser.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import logging + +from license_expression import get_spdx_licensing +from licomp.interface import LicompException +from licomp.return_codes import ReturnCodes + +AND = "AND" +OR = "OR" + +COMPATIBILITY_TYPE = 'compatibility_type' +COMPATIBILITY_OUTBOUND_LICENSE = 'outbound_license' +COMPATIBILITY_INBOUND_LICENSE = 'inbound_license' + +class LicenseExpressionParser(): + + def __init__(self): + self.licensing = get_spdx_licensing() + + self.CLOSE_PARENTHESIS = ")" + self.LICENSE_SYMBOL = "LicenseSymbol" + self.LICENSE_WITH_SYMBOL = "LicenseWithExceptionSymbol" + + def parse_license_expression(self, expression): + if not expression: + raise LicompException("No license provided: " + str(expression), ReturnCodes.LICOMP_PARSE_ERROR) + + p = self.__parse_expression(self.licensing.parse(expression).pretty().replace('\n', ' ')) + return p + + def __is_license_with_exception(self, expression): + return expression.strip().startswith(self.LICENSE_WITH_SYMBOL) + + def __is_license(self, expression, with_exception=False): + if with_exception: + return expression.strip().startswith(self.LICENSE_SYMBOL) or expression.strip().startswith(self.LICENSE_WITH_SYMBOL) + return expression.strip().startswith(self.LICENSE_SYMBOL) + + def __is_operator(self, expression): + return expression.startswith(AND) or expression.startswith(OR) + + def __get_operator(self, expression): + if expression.startswith(AND): + return AND + if expression.startswith(OR): + return OR + raise Exception("BAD EXPRESSION----") + + def __get_operands_string(self, expression): + # length of the operator and parenthesis + op = self.__get_operator(expression) + op_size = len(op) + 1 + + # nr characters until closing (operator) parenthesis + left_parens = 1 + operand_size = 1 + for c in expression[op_size:]: + operand_size += 1 + if c == '(': + left_parens += 1 + elif c == ')': + left_parens -= 1 + + if left_parens == 0: + break + + rest = expression[op_size:operand_size + 1] + remains = expression[operand_size + 4:] + return rest, remains + + def is_close(self, expression): + return expression.startswith(self.CLOSE_PARENTHESIS) + + def __cleanup_license(self, operand): + stripped_operand = operand.strip() + + if self.__is_license_with_exception(operand): + trimmed_operand = stripped_operand.replace(f"{self.LICENSE_WITH_SYMBOL}('", '', 1) + else: + trimmed_operand = stripped_operand.replace(f"{self.LICENSE_SYMBOL}('", '', 1) + closing_paren_index = trimmed_operand.find(")") + op = trimmed_operand[:closing_paren_index - 1] + remains = trimmed_operand[closing_paren_index + 1:].strip() + if remains.startswith(","): + remains = remains[1:] + return op, remains.strip() + + def __parse_expression(self, expression): + logging.debug(f'__parse_expression: {expression}') + + if self.__is_operator(expression): + operator = self.__get_operator(expression) + operands = [] + ops, remains = self.__get_operands_string(expression) + while ops != "": + if self.__is_license(ops.strip(), with_exception=True): + operand, rem = self.__cleanup_license(ops) + operands.append({ + COMPATIBILITY_TYPE: "license", + 'license': operand, + }) + ops = rem + + elif self.__is_operator(ops.strip()): + operand = self.__parse_expression(ops.strip()) + _ops, _remains = self.__get_operands_string(ops.strip()) + operands.append(operand) + ops = _remains + + else: + raise LicompException(f'Failed parsing expression "{ops}". Complete expression "{expression}"', ReturnCodes.LICOMP_PARSE_ERROR) + return { + COMPATIBILITY_TYPE: 'expression', + "operator": operator, + "operands": operands, + } + + elif self.__is_license(expression, with_exception=True): + cleaned_up, rem = self.__cleanup_license(expression.strip()) + return { + COMPATIBILITY_TYPE: "license", + 'license': cleaned_up, + } + + elif self.__is_close(expression): + return "" + + raise Exception("Bottom reached") + + def to_string(self, parsed_license): + license_type = parsed_license['compatibility_type'] + if license_type == 'license': + return parsed_license['license'] + + operator = parsed_license['operator'] + license_expression = [] + for operand in parsed_license['operands']: + license_expression.append(f' ( {self.to_string(operand)} ) ') + return str(self.licensing.parse(operator.join(license_expression))) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py new file mode 100644 index 0000000..7b7b0f6 --- /dev/null +++ b/licomp_toolkit/format.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json + +class LicompToolkitFormatter(): + + @staticmethod + def formatter(fmt): + if fmt.lower() == 'json': + return JsonLicompToolkitFormatter() + if fmt.lower() == 'text': + return TextLicompToolkitFormatter() + + def format_compatibilities(self, compat): + return None + + def format_licomp_resources(self, licomp_resources): + return None + + def format_licomp_licenses(self, licomp_licenses): + return None + + def format_licomp_versions(self, licomp_versions): + return None + +class JsonLicompToolkitFormatter(): + + def format_compatibilities(self, compat): + return json.dumps(compat, indent=4) + + def format_licomp_resources(self, licomp_resources): + return json.dumps(licomp_resources, indent=4) + + def format_licomp_licenses(self, licomp_licenses): + return json.dumps(licomp_licenses, indent=4) + + def format_licomp_versions(self, licomp_versions): + return json.dumps(licomp_versions, indent=4) + +class TextLicompToolkitFormatter(): + + def format_licomp_resources(self, licomp_resources): + return "\n".join(licomp_resources) + + def format_licomp_licenses(self, licomp_licenses): + return "\n".join(licomp_licenses) + + def format_compatibilities(self, compat): + summary = compat['summary'] + output = [] + nr_valid = summary['results']['nr_valid'] + output.append(f'{nr_valid} succesfull response(s)') + if int(nr_valid) > 0: + output.append('Results:') + statuses = summary['compatibility_statuses'] + for status in statuses.keys(): + output.append(f' {status}: {", ".join(statuses[status])}') + return "\n".join(output) + + def format_licomp_versions(self, licomp_versions): + lt = 'licomp-toolkit' + res = [f'{lt}: {licomp_versions[lt]}'] + for k, v in licomp_versions['licomp-resources'].items(): + res.append(f'{k}: {v}') + return '\n'.join(res) diff --git a/licomp_toolkit/lic_expr.py b/licomp_toolkit/lic_expr.py index fe37958..f396a2c 100644 --- a/licomp_toolkit/lic_expr.py +++ b/licomp_toolkit/lic_expr.py @@ -2,17 +2,19 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import json - -# import logging +import logging from license_expression import get_spdx_licensing from licomp_toolkit.toolkit import LicompToolkit -# from licomp.interface import UseCase -# from licomp.interface import Provisioning +from licomp.interface import UseCase +from licomp.interface import Provisioning AND = "AND" OR = "OR" +COMPATIBILITY_TYPE = 'compatibility_type' +COMPATIBILITY_OUTBOUND_LICENSE = 'outbound_license' +COMPATIBILITY_INBOUND_LICENSE = 'inbound_license' + class LicenseExpressionParser(): def __init__(self): @@ -23,9 +25,7 @@ def __init__(self): self.LICENSE_WITH_SYMBOL = "LicenseWithExceptionSymbol" def parse_license_expression(self, expression): - print(" ---------------------- " + expression + "-------------------------") p = self.__parse_expression(self.licensing.parse(expression).pretty().replace('\n', ' ')) - print(" ---------------------- " + expression + "------------------------->> \n" + json.dumps(p, indent=4)) return p def __is_license_with_exception(self, expression): @@ -54,9 +54,7 @@ def __get_operands_string(self, expression): # nr characters until closing (operator) parenthesis left_parens = 1 operand_size = 1 - print("identify idx: " + expression[op_size:]) for c in expression[op_size:]: - # print("at " + c + ": " + str(operand_size)) operand_size += 1 if c == '(': left_parens += 1 @@ -68,9 +66,6 @@ def __get_operands_string(self, expression): rest = expression[op_size:operand_size + 1] remains = expression[operand_size - 1] - print("expression : " + expression) - print("operands : " + rest) - print("remains : " + remains) return rest, remains def is_close(self, expression): @@ -78,83 +73,78 @@ def is_close(self, expression): def __cleanup_license(self, operand): stripped_operand = operand.strip() - print("OPERself.AND: " + operand) if self.__is_license_with_exception(operand): trimmed_operand = stripped_operand.replace(f"{self.LICENSE_WITH_SYMBOL}('", '', 1) else: trimmed_operand = stripped_operand.replace(f"{self.LICENSE_SYMBOL}('", '', 1) - # print("TRIMMED: " + trimmed_operand) closing_paren_index = trimmed_operand.find(")") - # print(" CLEANEDUP to: >" + trimmed_operand + "<") - # print(" CLEANEDUP idx: " + str(closing_paren_index)) op = trimmed_operand[:closing_paren_index - 1] remains = trimmed_operand[closing_paren_index + 1:].strip() - print(" CLEANEDUP rem:>" + remains) if remains.startswith(","): remains = remains[1:] - # print(" CLEANEDUP op: " + op) - # print(" CLEANEDUP rem:>" + remains + "<") return op, remains.strip() def __parse_expression(self, expression): - print("pe: " + expression) + logging.debug(f'__parse_expression: {expression}') if self.__is_operator(expression): operator = self.__get_operator(expression) operands = [] - print("GETTING OPERself.ANDS FROM: " + expression) ops, remains = self.__get_operands_string(expression) while ops != "": - print("PARSE: " + ops) - print("PARSE: " + ops.strip()) if self.__is_license(ops.strip(), with_exception=True): operand, rem = self.__cleanup_license(ops) operands.append({ - "type": "license", - "license": operand, + COMPATIBILITY_TYPE: "license", + 'license': operand, }) ops = rem elif self.__is_operator(ops.strip()): operand = self.__parse_expression(ops.strip()) operands.append(operand) - print("OP ADD: " + str(operand)) ops = "" else: print("uh oh ... " + str(ops)) import sys sys.exit(1) - print("OP RET: " + str(operand)) return { - "type": "operator", + COMPATIBILITY_TYPE: "operator", "operator": operator, "operands": operands, } elif self.__is_license(expression, with_exception=True): - # TODO: what if exception??? cleaned_up, rem = self.__cleanup_license(expression.strip()) return { - "type": "license", - "license": cleaned_up, + COMPATIBILITY_TYPE: "license", + 'license': cleaned_up, } elif self.__is_close(expression): - print("op <--- CLOSE") return "" raise Exception("Bottom reached") + def to_string(self, parsed_license): + license_type = parsed_license['compatibility_type'] + if license_type == 'license': + return parsed_license['license'] + + operator = parsed_license['operator'] + license_expression = [] + for operand in parsed_license['operands']: + license_expression.append(f' ( {self.to_string(operand)} ) ') + return str(self.licensing.parse(operator.join(license_expression))) + class LicenseExpressionChecker(): def outbound_inbound_compatibility(self, outbound, lic): licomp = LicompToolkit() - print("out: " + str(outbound)) - print("in: " + str(lic)) return licomp.outbound_inbound_compatibility(outbound, lic, usecase="library", @@ -162,7 +152,6 @@ def outbound_inbound_compatibility(self, outbound, lic): def __compatibility_status(self, compatibility): status = compatibility['summary']['results'] - # nr_valid = status['nr_valid'] rets = [] for ret in status: @@ -170,46 +159,49 @@ def __compatibility_status(self, compatibility): continue rets.append(ret) - print("________________________________________________status: " + str(status)) - print("________________________________________________status: " + str(rets)) - - # status: {'nr_valid': '5', 'yes': {'count': 5, 'percent': 100.0}} - if len(rets) == 1: - print("RETURN: " + str(rets[0])) return rets[0] - print("RETURN: " + str(rets)) - print("RETURN: " + str(compatibility)) return "yes" def check_compatibility(self, outbound, parsed_expression, detailed_report=False): - if parsed_expression['type'] == 'license': - print(f' license: {parsed_expression}') + compat_object = { + COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], + 'compatiblity_check': 'outbound-operator -> inbound-license', + } + + if parsed_expression[COMPATIBILITY_TYPE] == 'license': + compat_object['compatiblity_check'] = 'outbound-license -> inbound-license' lic = parsed_expression['license'] compat = self.outbound_inbound_compatibility(outbound, lic) - parsed_expression['compatibility'] = self.__compatibility_status(compat) + compat_object['compatibility'] = self.__compatibility_status(compat) if detailed_report: - parsed_expression['compatibility_details'] = compat - parsed_expression['outbound'] = outbound - print("Added compat: " + str(compat)) + compat_object['compatibility_details'] = compat + + compat_object['inbound_license'] = lic + compat_object['outbound_license'] = outbound - return parsed_expression else: operator = parsed_expression['operator'] operands = parsed_expression['operands'] + compat_object['compatibility_object'] = { + 'operator': operator, + 'operands': [], + } + operands_object = [] for operand in operands: - print(f'hi yall {operator}: {operand}') - self.check_compatibility(outbound, operand, detailed_report=detailed_report) - # compat = summarise_compatibilities(operator, operand) - # operand['compatibility'] = "compat" - # print("Added compat: " + str(compat)) - operand['outbound'] = outbound + operand_compat = self.check_compatibility(outbound, operand, detailed_report=detailed_report) + operand_object = { + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'], + } + operands_object.append(operand_object) + + compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object) + compat_object['compatibility_object']['operands'] = operands_object - parsed_expression["outbound"] = outbound - parsed_expression["compatibility"] = self.summarise_compatibilities(operator, operands) - return parsed_expression + return compat_object def __init_summary(self, operands): summary = { @@ -219,17 +211,13 @@ def __init_summary(self, operands): "unknown": 0, } for operand in operands: - print("_init_summary: " + str(operand)) compat = operand['compatibility'] summary[compat] = summary[compat] + 1 return summary def __summarise_compatibilities_and(self, operands): nr_operands = len(operands) - for operand in operands: - print("OP: " + str(operand)) summary = self.__init_summary(operands) - print("len: " + str(nr_operands)) if summary['no'] != 0: return 'no' @@ -250,10 +238,9 @@ def __summarise_compatibilities_or(self, operands): def summarise_compatibilities(self, operator, operands): return { AND: self.__summarise_compatibilities_and, - OR: self.__summarise_compatibilities_or + OR: self.__summarise_compatibilities_or, }[operator](operands) - class ExpressionExpressionChecker(): def __init__(self): @@ -261,28 +248,42 @@ def __init__(self): self.le_parser = LicenseExpressionParser() def __parsed_expression_to_name(self, parsed_expression): - return parsed_expression[parsed_expression['type']] + return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] + + def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=False): - def check_compatibility(self, outbound, inbound, detailed_report=False): inbound_parsed = self.le_parser.parse_license_expression(inbound) outbound_parsed = self.le_parser.parse_license_expression(outbound) compatibility_report = self.__check_compatibility(outbound_parsed, inbound_parsed, + usecase, + provisioning, detailed_report) return { 'inbound': inbound, 'outbound': outbound, - 'compatibility_report': compatibility_report + 'usecase': UseCase.usecase_to_string(usecase), + 'provisioning': Provisioning.provisioning_to_string(provisioning), + 'compatibility': compatibility_report['compatibility'], + 'compatibility_type': compatibility_report['compatibility_type'], + 'compatibility_check': f'outbound-{compatibility_report["compatibility_type"]} -> inbound-{inbound_parsed["compatibility_type"]}', + 'compatibility_report': compatibility_report, } - def __check_compatibility(self, outbound_parsed, inbound_parsed, detailed_report=False): + def __check_compatibility(self, outbound_parsed, inbound_parsed, usecase, provisioning, detailed_report=False): + + outbound_type = outbound_parsed[COMPATIBILITY_TYPE] + compat_object = { + COMPATIBILITY_TYPE: outbound_type, + 'inbound_license': self.le_parser.to_string(inbound_parsed), + 'outbound_license': self.le_parser.to_string(outbound_parsed), + } - outbound_type = outbound_parsed['type'] if outbound_type == 'license': - print(f' license: {outbound_parsed}') + compat_object['compatiblity_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]}' outbound_parsed_license = outbound_parsed['license'] # Check if: # outbound license @@ -291,53 +292,35 @@ def __check_compatibility(self, outbound_parsed, inbound_parsed, detailed_report compat = self.le_checker.check_compatibility(outbound_parsed_license, inbound_parsed, detailed_report) + compat_object['compatibility'] = compat['compatibility'] + compat_object['compatibility_details'] = compat - outbound_parsed['compatibility'] = compat['compatibility'] - # TODO: bring back details - # outbound_parsed['compatibility_details'] = compat - - return compat - - if outbound_type == 'operator': - outbound_parsed_operator = outbound_parsed['operator'] - print(f' operator: {outbound_parsed_operator}') + elif outbound_type == 'operator': + compat_object['compatiblity_check'] = f'outbound-operator -> inbound-{inbound_parsed["compatibility_type"]}' operator = outbound_parsed['operator'] operands = outbound_parsed['operands'] + compat_object['compatibility_object'] = { + 'operator': operator, + 'operands': [], + } + + operands_object = [] for operand in operands: # Check if: # operand from outbound license # is compatible with # inbound license - inbound_compat = self.__check_compatibility(operand, + operand_compat = self.__check_compatibility(operand, inbound_parsed, detailed_report) - # operand['compatibility_details'] = inbound_compat - - operand['inbound_compatibility'] = inbound_parsed - operand['compatibility'] = inbound_compat['compatibility'] - - outbound_parsed['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands) - - # TODO: bring back details - - return outbound_parsed - - return " WOOPS" - -# parser = LicenseExpressionParser() - - -expr_checker = ExpressionExpressionChecker() + operand_object = { + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'], + } + operands_object.append(operand_object) -inbound = "MIT OR Apache-2.0" -outbound = "GPL-2.0-only AND BSD-2-Clause" -checked = expr_checker.check_compatibility(outbound, - inbound, - detailed_report=False) + compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object) + compat_object['compatibility_object']['operands'] = operands_object -print("------------------------------------------") -print() -print() -print() -print(json.dumps(checked, indent=4)) + return compat_object diff --git a/licomp_toolkit/schema_checker.py b/licomp_toolkit/schema_checker.py new file mode 100644 index 0000000..142c0b5 --- /dev/null +++ b/licomp_toolkit/schema_checker.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import jsonschema +import logging +import os + +from licomp_toolkit.toolkit import LicompToolkit +from licomp.interface import LicompException + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +DATA_DIR = os.path.realpath(os.path.join(SCRIPT_DIR, "data")) +SCHEMA_FILE = os.path.realpath(os.path.join(DATA_DIR, "reply_schema.json")) + +class LicompToolkitSchemaChecker: + + def __init__(self): + with open(SCHEMA_FILE) as fp: + self.expr_expr_schema = json.load(fp) + + def __validate_deeply(self, compat): + validations = 0 + lt = LicompToolkit() + + compat_check = compat['compatibility_check'] + if compat_check == 'outbound-license -> inbound-license': + compat_object = compat['compatibility_object'] + if not compat_object: + details = compat['compatibility_details'] + else: + details = compat_object['compatibility_details'] + compatibilities = details['compatibilities'] + for compatibility_object in compatibilities: + logging.debug(f' {compatibility_object["resource_name"]}') + inner_validations = lt.validate(compatibility_object) + validations += 1 + logging.debug('Validation OK') + return validations + else: + + if compat_check == 'outbound-expression -> inbound-license' or compat_check == 'outbound-expression -> inbound-expression': + compat_object = compat + elif compat['compatibility_check'] == 'outbound-license -> inbound-expression': + compat_object = compat['compatibility_object'] + else: + raise LicompException("Validation failed. Invalid state: " + compat_check) + + for operand in compat_object['operands']: + operand_compat_object = operand['compatibility_object'] + inner_validations = self.__validate_deeply(operand_compat_object) + validations += inner_validations + return validations + + def validate(self, content, deep=False): + jsonschema.validate(instance=content, + schema=self.expr_expr_schema) + validations = 1 + if deep: + report = content['compatibility_report'] + validations = self.__validate_deeply(report) + return validations + + def validate_file(self, filename, deep=False): + with open(filename) as fp: + return self.validate(json.load(fp), deep) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index e9f6c44..4852182 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -2,8 +2,6 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import importlib -import json import logging from licomp.interface import Licomp @@ -11,6 +9,7 @@ from licomp.interface import Provisioning from licomp.interface import LicompException from licomp.return_codes import ReturnCodes +from licomp.interface import CompatibilityStatus from licomp_osadl.osadl import LicompOsadl from licomp_reclicense.reclicense import LicompReclicense @@ -23,62 +22,17 @@ from licomp_toolkit.config import licomp_toolkit_version from licomp_toolkit.config import cli_name -class LicompToolkitFormatter(): +from licomp_toolkit.expr_parser import LicenseExpressionParser +from licomp_toolkit.expr_parser import COMPATIBILITY_TYPE +from licomp_toolkit.expr_parser import AND +from licomp_toolkit.expr_parser import OR - @staticmethod - def formatter(fmt): - if fmt.lower() == 'json': - return JsonLicompToolkitFormatter() - if fmt.lower() == 'text': - return TextLicompToolkitFormatter() - - def format_compatibilities(self, compat): - return None - - def format_licomp_resources(self, licomp_resources): - return None - - def format_licomp_versions(self, licomp_versions): - return None - -class JsonLicompToolkitFormatter(): - - def format_compatibilities(self, compat): - return json.dumps(compat, indent=4) - - def format_licomp_resources(self, licomp_resources): - return json.dumps(licomp_resources, indent=4) - - def format_licomp_versions(self, licomp_versions): - return json.dumps(licomp_versions, indent=4) - -class TextLicompToolkitFormatter(): - - def format_licomp_resources(self, licomp_resources): - return "\n".join(licomp_resources) - - def format_compatibilities(self, compat): - summary = compat['summary'] - output = [] - nr_valid = summary['results']['nr_valid'] - output.append(f'{nr_valid} succesfull response(s)') - if int(nr_valid) > 0: - output.append('Results:') - statuses = summary['compatibility_statuses'] - for status in statuses.keys(): - output.append(f' {status}: {", ".join(statuses[status])}') - return "\n".join(output) - - def format_licomp_versions(self, licomp_versions): - lt = 'licomp-toolkit' - res = [f'{lt}: {licomp_versions[lt]}'] - for k, v in licomp_versions['licomp-resources'].items(): - res.append(f'{k}: {v}') - return '\n'.join(res) +from licomp_toolkit.config import my_supported_api_version class LicompToolkit(Licomp): def __init__(self): + Licomp.__init__(self) self.LICOMP_RESOURCES = {} self.LICOMP_RESOURCE_NAMES = { "osadl": { @@ -99,6 +53,9 @@ def __init__(self): }, } + def supported_api_version(self): + return my_supported_api_version + def __add_to_list(self, store, data, name): if not data: return @@ -122,11 +79,12 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, statuses = {} compats = {} compatibilities['nr_licomp'] = len(self.licomp_resources()) - for resource_name in self.licomp_resources(): - compat = compatibilities["compatibilities"][resource_name] + # for resource_name in self.licomp_resources(): + for compat in compatibilities["compatibilities"]: + logging.debug(f': {compat}') logging.debug(f': {compat["resource_name"]}') - self.__add_to_list(statuses, compat['status'], compat['resource_name']) - self.__add_to_list(compats, compat['compatibility_status'], compat['resource_name']) + self.__add_to_list(statuses, compat['status'], compat) + self.__add_to_list(compats, compat['compatibility_status'], compat) compatibilities["summary"]["resources"] = [f'{x.name()}:{x.version()}' for x in self.licomp_resources().values()] compatibilities["summary"]["outbound"] = outbound compatibilities["summary"]["inbound"] = inbound @@ -159,26 +117,15 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning): logging.debug(f'{inbound} {outbound} ') - # Check usecase - try: - usecase = UseCase.string_to_usecase(usecase) - except KeyError: - raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE) - - # Check provisioning - try: - provisioning = Provisioning.string_to_provisioning(provisioning) - except KeyError: - raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING) - compatibilities = {} - compatibilities['compatibilities'] = {} + compatibilities['compatibilities'] = [] for resource_name in self.licomp_resources(): resource = self.licomp_resources()[resource_name] logging.debug(f'-- resource: {resource.name()}') + compat = resource.outbound_inbound_compatibility(outbound, inbound, usecase, provisioning=provisioning) - compatibilities['compatibilities'][compat['resource_name']] = compat + compatibilities['compatibilities'].append(compat) self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning) self.__add_meta(compatibilities) @@ -223,26 +170,225 @@ def versions(self, verbose=False): def name(self): return cli_name -def __class_instance(package, class_name): - licomp_resource = importlib.import_resource(f'{package}') - licomp_class = getattr(licomp_resource, class_name) - return licomp_class() - -def __check_api_version(subclass): - licomp_api_version = Licomp.api_version() - subclass_api_version = subclass.supported_api_version() - logging.debug(f'{licomp_api_version} == {subclass_api_version} ???') - - licomp_api_version_major = licomp_api_version.split('.')[0] - licomp_api_version_minor = licomp_api_version.split('.')[1] - - subclass_api_version_major = subclass_api_version.split('.')[0] - subclass_api_version_minor = subclass_api_version.split('.')[1] - assert licomp_api_version_major == subclass_api_version_major # noqa: S101 - assert licomp_api_version_minor == subclass_api_version_minor # noqa: S101 - -def _inc_map(_map, _name): - curr = _map.get(_name, 0) - new = curr + 1 - _map[_name] = new - return _map +class LicenseExpressionChecker(): + + def __init__(self): + self.le_parser = LicenseExpressionParser() + self.licomp = LicompToolkit() + + def outbound_inbound_compatibility(self, outbound, lic, usecase, provisioning): + return self.licomp.outbound_inbound_compatibility(outbound, + lic, + usecase, + provisioning) + + def __compatibility_status(self, compatibility): + status = compatibility['summary']['results'] + rets = [] + for ret in status: + if ret == 'nr_valid': + continue + elif not ret: + pass + elif ret == CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED): + pass + else: + rets.append(ret) + + if len(rets) == 0: + return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED) + + if len(rets) == 1: + return rets[0] + + return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.MIXED) + + def check_compatibility(self, + outbound, + parsed_expression, + usecase, + provisioning, + detailed_report=True): + + compat_object = { + COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], + 'compatibility_check': 'outbound-expression -> inbound-license', + } + + if parsed_expression[COMPATIBILITY_TYPE] == 'license': + compat_object['compatibility_check'] = 'outbound-license -> inbound-license' + lic = parsed_expression['license'] + compat = self.outbound_inbound_compatibility(outbound, + lic, + usecase, + provisioning) + compat_object['compatibility'] = self.__compatibility_status(compat) + if detailed_report: + compat_object['compatibility_details'] = compat + else: + compat_object['compatibility_details'] = None + compat_object['inbound_license'] = lic + compat_object['outbound_license'] = outbound + compat_object['compatibility_object'] = {} + + else: + operator = parsed_expression['operator'] + operands = parsed_expression['operands'] + compat_object['operator'] = operator + + compat_object['inbound_license'] = self.le_parser.to_string(parsed_expression) + compat_object['outbound_license'] = outbound + compat_object['compatibility_details'] = None + operands_object = [] + for operand in operands: + operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, detailed_report=detailed_report) + operand_object = { + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'], + } + operands_object.append(operand_object) + + compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object) + compat_object['operands'] = operands_object + + return compat_object + + def __init_summary(self, operands): + summary = { + "yes": 0, + "no": 0, + "depends": 0, + "unknown": 0, + "unsupported": 0, + "mixed": 0, + } + for operand in operands: + compat = operand['compatibility'] + summary[compat] = summary[compat] + 1 + return summary + + def __summarise_compatibilities_and(self, operands): + nr_operands = len(operands) + summary = self.__init_summary(operands) + + if summary['no'] != 0: + return 'no' + + if summary['yes'] == nr_operands: + return "yes" + + return "no" + + def __summarise_compatibilities_or(self, operands): + summary = self.__init_summary(operands) + + if summary['yes'] != 0: + return 'yes' + + return "no" + + def summarise_compatibilities(self, operator, operands): + return { + AND: self.__summarise_compatibilities_and, + OR: self.__summarise_compatibilities_or, + }[operator](operands) + + +class ExpressionExpressionChecker(): + + def __init__(self): + self.le_checker = LicenseExpressionChecker() + self.le_parser = LicenseExpressionParser() + + def __parsed_expression_to_name(self, parsed_expression): + return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] + + def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=True): + # Check usecase + try: + usecase = UseCase.string_to_usecase(usecase) + except KeyError: + raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE) + + # Check provisioning + try: + provisioning = Provisioning.string_to_provisioning(provisioning) + except KeyError: + raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING) + + inbound_parsed = self.le_parser.parse_license_expression(inbound) + + outbound_parsed = self.le_parser.parse_license_expression(outbound) + compatibility_object = self.__check_compatibility(outbound_parsed, + inbound_parsed, + usecase, + provisioning, + detailed_report) + return { + 'inbound': inbound, + 'outbound': outbound, + 'usecase': UseCase.usecase_to_string(usecase), + 'provisioning': Provisioning.provisioning_to_string(provisioning), + 'compatibility': compatibility_object['compatibility'], + 'compatibility_report': compatibility_object, + } + + def __check_compatibility(self, + outbound_parsed, + inbound_parsed, + usecase, + provisioning, + detailed_report=True): + + outbound_type = outbound_parsed[COMPATIBILITY_TYPE] + compat_object = { + COMPATIBILITY_TYPE: outbound_type, + 'inbound_license': self.le_parser.to_string(inbound_parsed), + 'outbound_license': self.le_parser.to_string(outbound_parsed), + } + + if outbound_type == 'license': + compat_object['compatibility_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]}' + outbound_parsed_license = outbound_parsed['license'] + # Check if: + # outbound license + # is compatible with + # inbound license + compat = self.le_checker.check_compatibility(outbound_parsed_license, + inbound_parsed, + usecase, + provisioning, + detailed_report) + compat_object['compatibility'] = compat['compatibility'] + compat_object['compatibility_object'] = compat + compat_object['compatibility_details'] = None + + elif outbound_type == 'expression': + compat_object['compatibility_details'] = None + compat_object['compatibility_check'] = f'outbound-expression -> inbound-{inbound_parsed["compatibility_type"]}' + operator = outbound_parsed['operator'] + operands = outbound_parsed['operands'] + + compat_object['operator'] = operator + + operands_object = [] + for operand in operands: + # Check if: + # operand from outbound license + # is compatible with + # inbound license + operand_compat = self.__check_compatibility(operand, + inbound_parsed, + usecase, + provisioning, + detailed_report) + operand_object = { + 'compatibility_object': operand_compat, + 'compatibility': operand_compat['compatibility'], + } + operands_object.append(operand_object) + + compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object) + compat_object['operands'] = operands_object + + return compat_object diff --git a/licomp_toolkit/utils.py b/licomp_toolkit/utils.py index 9dea3aa..1597feb 100644 --- a/licomp_toolkit/utils.py +++ b/licomp_toolkit/utils.py @@ -2,6 +2,10 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import importlib +import logging + +from licomp.interface import Licomp from licomp.return_codes import compatibility_status_to_returncode from licomp.return_codes import ReturnCodes @@ -21,3 +25,27 @@ def licomp_results_to_return_code(licomp_results): return compatibility_status_to_returncode(result) return ReturnCodes.LICOMP_INTERNAL_ERROR.value + +def __class_instance(package, class_name): + licomp_resource = importlib.import_resource(f'{package}') + licomp_class = getattr(licomp_resource, class_name) + return licomp_class() + +def __check_api_version(subclass): + licomp_api_version = Licomp.api_version() + subclass_api_version = subclass.supported_api_version() + logging.debug(f'{licomp_api_version} == {subclass_api_version} ???') + + licomp_api_version_major = licomp_api_version.split('.')[0] + licomp_api_version_minor = licomp_api_version.split('.')[1] + + subclass_api_version_major = subclass_api_version.split('.')[0] + subclass_api_version_minor = subclass_api_version.split('.')[1] + assert licomp_api_version_major == subclass_api_version_major # noqa: S101 + assert licomp_api_version_minor == subclass_api_version_minor # noqa: S101 + +def _inc_map(_map, _name): + curr = _map.get(_name, 0) + new = curr + 1 + _map[_name] = new + return _map diff --git a/tests/python/test_expr_expr.py b/tests/python/test_expr_expr.py new file mode 100644 index 0000000..d52c0a8 --- /dev/null +++ b/tests/python/test_expr_expr.py @@ -0,0 +1,165 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import pytest +import logging +import sys + +from licomp.interface import Licomp +from licomp.interface import Provisioning +from licomp.interface import UseCase +from licomp.interface import CompatibilityStatus + +from licomp_toolkit.toolkit import ExpressionExpressionChecker + +eec = ExpressionExpressionChecker() + +GPLv2 = 'GPL-2.0-only' +MIT = 'MIT' +MIT_A_0BSD = 'MIT AND 0BSD' +APACHE2 = 'Apache-2.0' +APACHE2_A_ISC = 'Apache-2.0 AND ISC' +MIT_O_APACHE2 = 'MIT OR Apache-2.0' +MIT_A_APACHE2 = 'MIT AND Apache-2.0' +GPLv2_A_BSD3 = 'GPL-2.0-only AND BSD-3-Clause' + + +def _compat_status(report): + return report['compatibility_report']['compatibility'] + +def _compat_type(report): + return report['compatibility_report']['compatibility_type'] + +# +# license compat with license +# + +# GPL-2.0-only -> MIT are compatible +def test_lic_lic_compat(): + compat_report = eec.check_compatibility(GPLv2, MIT, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'license' + +# MIT -> GPL-2.0-only -> are NOT compatible +def test_lic_lic_incompat(): + compat_report = eec.check_compatibility(MIT, GPLv2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'license' + +# +# license compat with expression +# + +# GPL-2.0-only -> MIT OR Apache-2.0 are compatible +def test_lic_expr_compat(): + compat_report = eec.check_compatibility(GPLv2, MIT_O_APACHE2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'license' + +# GPL-2.0-only -> MIT AND Apache-2.0 are NOT compatible +def test_lic_expr_incompat(): + compat_report = eec.check_compatibility(GPLv2, MIT_A_APACHE2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'license' + +# +# expression compat with license +# +# GPL-2.0-only AND BSD-3-Clause -> MIT are compatible +def test_expr_lic_compat(): + compat_report = eec.check_compatibility(GPLv2_A_BSD3, MIT, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'expression' + +# GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 are NOT compatible +def test_expr_lic_incompat(): + compat_report = eec.check_compatibility(GPLv2_A_BSD3, APACHE2, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + +# +# expression compat with expression +# + +# GPL-2.0-only AND BSD-3-Clause -> MIT AND 0BSD are compatible +def test_expr_expr_compat(): + compat_report = eec.check_compatibility(GPLv2_A_BSD3, MIT_A_0BSD, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'expression' + +# GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 AND ISC are NOT compatible +def test_expr_expr_incompat(): + compat_report = eec.check_compatibility(GPLv2_A_BSD3, APACHE2_A_ISC, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + + + +OUTBOUND = f' ({GPLv2_A_BSD3} AND {GPLv2_A_BSD3} AND ({GPLv2_A_BSD3} AND ({GPLv2_A_BSD3} AND ({GPLv2_A_BSD3} AND {GPLv2_A_BSD3} ))) )' + +INBOUND = f' ( {MIT_A_0BSD} AND {MIT_A_0BSD} AND {MIT_A_0BSD} AND {MIT_A_0BSD} ) ' +INBOUND += f' AND ( {MIT_O_APACHE2} OR {MIT_O_APACHE2} OR {MIT_O_APACHE2} OR ( {MIT_O_APACHE2} OR {MIT_O_APACHE2} OR ( {MIT_O_APACHE2} AND {MIT_O_APACHE2} )) )' + +def test_expr_expr_large_compat(): + + compat_report = eec.check_compatibility(OUTBOUND, + INBOUND, + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'yes' + assert _compat_type(compat_report) == 'expression' + + +def test_expr_expr_large_incompat(): + + compat_report = eec.check_compatibility(OUTBOUND, + f'{INBOUND} AND {APACHE2}', + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + + + +def test_expr_expr_with_1(): + + # OSADL supports GPL-2.0-only WITH Classpath-exception-2.0 + # - usecase is SNIPPET + compat_report = eec.check_compatibility(OUTBOUND, + f'{INBOUND} AND GPL-2.0-only WITH Classpath-exception-2.0', + usecase=UseCase.usecase_to_string(UseCase.SNIPPET), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + + +def test_expr_expr_with_2(): + + compat_report = eec.check_compatibility(OUTBOUND, + f'{INBOUND} AND GPL-2.0-only WITH Classpath-exception-2.0 AND {APACHE2}', + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + assert _compat_status(compat_report) == 'no' + assert _compat_type(compat_report) == 'expression' + + + diff --git a/tests/python/test_expr_parser.py b/tests/python/test_expr_parser.py new file mode 100644 index 0000000..3171dd9 --- /dev/null +++ b/tests/python/test_expr_parser.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import pytest + +from licomp_toolkit.expr_parser import LicenseExpressionParser +from licomp.interface import LicompException + +parser = LicenseExpressionParser() + +def _type(expr): + return expr['compatibility_type'] + +def _is_expression(expr): + return expr['compatibility_type'] == 'expression' + +def _is_license(expr): + return expr['compatibility_type'] == 'license' + +def _license(expr): + return expr['license'] + +def _operator(expr): + return expr['operator'] + +def _operands(expr): + return expr['operands'] + +def test_none(): + with pytest.raises(LicompException): + expression = parser.parse_license_expression(None) + +def test_empty(): + with pytest.raises(LicompException): + expression = parser.parse_license_expression('') + +def test_single(): + expression = parser.parse_license_expression('MIT') + assert _is_license(expression) + assert _license(expression) == 'MIT' + +def test_single_with(): + expression = parser.parse_license_expression('GPL-3.0-only WITH GCC-exception-3.1') + assert _is_license(expression) + assert _license(expression) == 'GPL-3.0-only WITH GCC-exception-3.1' + +def test_simple_or(): + expression = parser.parse_license_expression('MIT OR X11') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_simple_or_with(): + expression = parser.parse_license_expression('MIT OR GPL-3.0-only WITH GCC-exception-3.1') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'GPL-3.0-only WITH GCC-exception-3.1', 'MIT' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_simple_or_with2(): + expression = parser.parse_license_expression('GPL-3.0-only WITH GCC-exception-3.1 OR MIT') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'GPL-3.0-only WITH GCC-exception-3.1', 'MIT' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_simple_and(): + expression = parser.parse_license_expression('MIT AND X11') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'AND' + +def test_simple_and_with(): + expression = parser.parse_license_expression('MIT AND GPL-3.0-only WITH GCC-exception-3.1') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'GPL-3.0-only WITH GCC-exception-3.1', 'MIT' ] + assert _is_expression(expression) + assert _operator(expression) == 'AND' + +def test_many_ors(): + expression = parser.parse_license_expression('MIT OR X11 OR BSD-3-Clause OR ISC') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'BSD-3-Clause', 'ISC', 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'OR' + +def test_many_ands(): + expression = parser.parse_license_expression('MIT AND X11 AND BSD-3-Clause AND ISC AND GPL-3.0-only WITH GCC-exception-3.1') + operands = [x['license'] for x in _operands(expression)] + operands.sort() + assert operands == [ 'BSD-3-Clause', 'GPL-3.0-only WITH GCC-exception-3.1', 'ISC', 'MIT', 'X11' ] + assert _is_expression(expression) + assert _operator(expression) == 'AND' + +def test_many_complex_expr(): + expression = parser.parse_license_expression('MIT AND X11 AND (BSD-3-Clause OR ( ISC OR GPL-3.0-only WITH GCC-exception-3.1) )') + assert _operator(expression) == 'AND' + operands = _operands(expression) + assert _license(operands[0]) == 'MIT' + assert _license(operands[1]) == 'X11' + assert _operator(operands[2]) == 'OR' + operands = _operands(operands[2]) + assert _license(operands[0]) == 'BSD-3-Clause' + assert _operator(operands[1]) == 'OR' + operands = _operands(operands[1]) + assert _license(operands[0]) == 'ISC' + assert _license(operands[1]) == 'GPL-3.0-only WITH GCC-exception-3.1' + + + diff --git a/tests/python/test_same_compats.py b/tests/python/test_same_compats.py index ff013f3..22514d9 100644 --- a/tests/python/test_same_compats.py +++ b/tests/python/test_same_compats.py @@ -46,8 +46,8 @@ def test_supported(): for out_lic in licenses: ret = lt.outbound_inbound_compatibility(out_lic, in_lic, - UseCase.usecase_to_string(usecase), - Provisioning.provisioning_to_string(provisioning)) + usecase, + provisioning) results = ret['summary']['results'] if int(results['nr_valid']) == 0: unsupported += 1 diff --git a/tests/python/test_toolkit.py b/tests/python/test_toolkit.py index e61c5f4..03c4761 100644 --- a/tests/python/test_toolkit.py +++ b/tests/python/test_toolkit.py @@ -30,18 +30,18 @@ def test_provisioning_is_not_supported(): assert not lt.provisioning_supported(provisioning=Provisioning.WEBUI) def test_compat(): - ret = lt.outbound_inbound_compatibility("GPL-2.0-only", "BSD-3-Clause", UseCase.usecase_to_string(UseCase.LIBRARY), Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + ret = lt.outbound_inbound_compatibility("GPL-2.0-only", "BSD-3-Clause", UseCase.LIBRARY, Provisioning.BIN_DIST) logging.debug("ret: " + str(ret['summary']['results'])) print("... hesa: " + str(ret['summary']['results']['yes'])) assert ret['summary']['results']['yes']['count'] == 4 def test_incompat(): - ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.usecase_to_string(UseCase.LIBRARY), Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) + ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.LIBRARY, Provisioning.BIN_DIST) logging.debug("ret: " + str(ret['summary']['results'])) assert ret['summary']['results']['no']['count'] == 2 def test_incompat(): - ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.usecase_to_string(UseCase.LIBRARY), Provisioning.provisioning_to_string(Provisioning.WEBUI)) + ret = lt.outbound_inbound_compatibility("BSD-3-Clause", "GPL-2.0-only", UseCase.LIBRARY, Provisioning.WEBUI) logging.debug("ret: " + str(ret['summary']['statuses'])) # all five resources fail on webui assert len(ret['summary']['statuses']['failure']) == 6 diff --git a/tests/python/test_validator.py b/tests/python/test_validator.py new file mode 100644 index 0000000..c9d43ae --- /dev/null +++ b/tests/python/test_validator.py @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import pytest +import logging +import sys + +from licomp.interface import Licomp +from licomp.interface import Provisioning +from licomp.interface import UseCase +from licomp.interface import CompatibilityStatus + +from licomp_toolkit.toolkit import ExpressionExpressionChecker +from licomp_toolkit.schema_checker import LicompToolkitSchemaChecker + +eec = ExpressionExpressionChecker() +checker = LicompToolkitSchemaChecker() + +def test_lic_lic(): + compat_report = eec.check_compatibility("MIT", "BSD-3-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating simple expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating simple expression (deep): " + str(ret)) + +def test_lic_expr(): + compat_report = eec.check_compatibility("MIT", "BSD-3-Clause OR BSD-2-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating license->expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating license->expression (deep): " + str(ret)) + + +def test_expr_lic(): + compat_report = eec.check_compatibility("MIT OR X11", "BSD-3-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating expression->license (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating expression->license (deep): " + str(ret)) + + +def test_expr_expr(): + compat_report = eec.check_compatibility("MIT OR X11", "BSD-3-Clause OR BSD-2-Clause", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating expression->expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating expression->expression (deep): " + str(ret)) + +def test_expr_expr_many(): + compat_report = eec.check_compatibility("MIT OR X11 OR ISC AND MIT-0", "BSD-3-Clause OR BSD-2-Clause OR 0BSD AND Apache-2.0 AND 0BSD", + usecase=UseCase.usecase_to_string(UseCase.LIBRARY), + provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST), + detailed_report=True) + + ret = checker.validate(compat_report) + print("Validating expression->expression (non deep): " + str(ret)) + + ret = checker.validate(compat_report, deep=True) + print("Validating expression->expression (deep): " + str(ret)) + diff --git a/tests/shell/test-cli.sh b/tests/shell/test-cli.sh index dd8d39e..cc5f975 100755 --- a/tests/shell/test-cli.sh +++ b/tests/shell/test-cli.sh @@ -6,6 +6,8 @@ LT_VERSION=$(grep licomp_toolkit_version licomp_toolkit/config.py | cut -d = -f 2 | sed "s,[' ]*,,g") +EXTRACT_COMPAT=".compatibility_report.compatibility_object.compatibility_details" + if [ "$1" == "--local" ] then IMPLEMENTATIONS=../licomp:../licomp-dwheeler:../licomp-hermione:../licomp-osadl:../licomp-reclicense:../licomp-proprietary::../licomp-gnuguide:. @@ -70,30 +72,30 @@ test_version() test_supp_unsupp() { echo "# test supported/unsupported licenses" - test_licomp_tk "verify -il MIT -ol MIT" ".summary.results.yes.count" 5 - test_licomp_tk "verify -il MIT -ol MIT2" ".summary.results.nr_valid" 0 - test_licomp_tk "verify -il MIT2 -ol MIT" ".summary.results.nr_valid" 0 - test_licomp_tk "verify -il MIT2 -ol MIT2" ".summary.results.nr_valid" 0 + test_licomp_tk "verify -il MIT -ol MIT" "${EXTRACT_COMPAT}.summary.results.yes.count" 5 + test_licomp_tk "verify -il MIT -ol MIT2" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 + test_licomp_tk "verify -il MIT2 -ol MIT" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 + test_licomp_tk "verify -il MIT2 -ol MIT2" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 } test_snippets() { echo "# test snippets only" - test_licomp_tk "-u snippet verify -il MIT -ol MIT" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il MIT -ol MIT2" ".summary.results.nr_valid" 0 - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.yes.count" 1 - test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" ".summary.results.yes.count" null + test_licomp_tk "-u snippet verify -il MIT -ol MIT" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il MIT -ol MIT2" "${EXTRACT_COMPAT}.summary.results.nr_valid" 0 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.yes.count" 1 + test_licomp_tk "-u snippet verify -il LGPL-2.1-or-later -ol BSD-3-Clause" "${EXTRACT_COMPAT}.summary.results.yes.count" null } test_snippet_bindist() { echo "# snippet vs bin dist" - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.nr_valid" 1 - test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.yes.count" 1 - test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.nr_valid" 4 - test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" ".summary.results.yes.count" 4 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.nr_valid" 1 + test_licomp_tk "-u snippet verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.yes.count" 1 + test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.nr_valid" 4 + test_licomp_tk "verify -il BSD-3-Clause -ol LGPL-2.1-or-later" "${EXTRACT_COMPAT}.summary.results.yes.count" 4 } test_supports_license() diff --git a/tests/shell/test_returns.sh b/tests/shell/test_returns.sh index bc9f1fc..460ea2f 100755 --- a/tests/shell/test_returns.sh +++ b/tests/shell/test_returns.sh @@ -47,7 +47,7 @@ test_verify() run_comp_test 0 "verify -il BSD-3-Clause -ol GPL-2.0-only" # Success and mixed compatibility - run_comp_test 1 "verify -ol 0BSD -il MS-PL" + run_comp_test 9 "verify -ol 0BSD -il MS-PL" # Success and incompatible run_comp_test 2 "verify -il GPL-2.0-only -ol BSD-3-Clause" diff --git a/tests/shell/test_validate.sh b/tests/shell/test_validate.sh new file mode 100755 index 0000000..b0ba71f --- /dev/null +++ b/tests/shell/test_validate.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +if [ "$1" == "--local" ] +then + IMPLEMENTATIONS=../licomp:../licomp-dwheeler:../licomp-hermione:../licomp-osadl:../licomp-reclicense:../licomp-proprietary::../licomp-gnuguide:. + shift +fi + + +check_return_value() +{ + EXPEXcTED=$1 + ACTUAL=$2 + COMMAND="$3" + + if [ $EXPECTED -ne $ACTUAL ] + then + echo "ERROR" + echo "Return values differ" + echo " Expected: $EXPECTED" + echo " Actual: $ACTUAL" + echo " Command: $COMMAND" + exit 1 + fi +} + + +REPLY_FILE=licomp-toolkit-reply.json + + +validate_reply() +{ + INBOUND="$1" + OUTBOUND="$2" + EXPECTED=$3 + PYTHONPATH=$IMPLEMENTATIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py verify -il "$INBOUND" -ol "$OUTBOUND" > $REPLY_FILE + PYTHONPATH=$IMPLEMENTATIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py validate $REPLY_FILE + RET=$? + printf "%-75s" "reply from verify -il \"$INBOUND\" -ol \"$OUTBOUND\"" + check_return_value $RET $EXPECTED "validate $REPLY_FILE (verify -il \"$INBOUND\" -ol \"$OUTBOUND\")" + echo OK +} + +validate_reply MIT MIT 0 +validate_reply MIT BSD-3-Clause 0 +validate_reply MIT "BSD-3-Clause OR MIT" 0 +validate_reply "BSD-3-Clause OR MIT" MIT 0 +validate_reply "BSD-3-Clause OR MIT" "X11 AND ISC" 0 + +exit +rm $REPLY_FILE