From c472fe4b3fa190fef73aaa0110ce45f3fb65d063 Mon Sep 17 00:00:00 2001 From: Jinguang Dong Date: Sat, 23 Nov 2024 11:51:01 +0800 Subject: [PATCH] Description:[feature]optimize description for readme opensrouce tools Bug: https://gitee.com/openharmony/developtools_integration_verification/issues/IB6IUH Test: The unit test can run through Signed-off-by: Jinguang Dong --- tools/opensource_tools/README_OSS.md | 271 ++++--------- .../src/generate_readme_opensource.py | 84 ++++- .../src/spdx_license_matcher.py | 15 + .../src/validate_readme_opensource.py | 81 +++- .../test/test_generate_readme_opensource.py | 236 ++++++++---- .../test/test_spdx_license_matcher.py | 15 + .../test/test_validate_readme_opensource.py | 357 +++++++++++------- 7 files changed, 642 insertions(+), 417 deletions(-) diff --git a/tools/opensource_tools/README_OSS.md b/tools/opensource_tools/README_OSS.md index 1ae189c..b7c21fd 100644 --- a/tools/opensource_tools/README_OSS.md +++ b/tools/opensource_tools/README_OSS.md @@ -14,11 +14,6 @@ - 验证格式 - 验证内容 - 命令行参数 - - 测试与验证 - - 自动化测试 - - 运行测试 - - 预期结果 - - 手动测试 - 目录结构 - 工作流程概览 - 生成工具流程图 @@ -32,11 +27,16 @@ - 生成工具 : - 通过交互方式,用户输入开源部件的信息,支持多个部件的输入。 + - 支持一对一、一对多、多对一的许可证和许可证文件关系配置。 + - 支持可选的依赖项配置,以逗号分隔多个依赖。 - 生成符合规范的 `README.OpenSource` 文件,包含所有输入的部件信息。 - 验证工具: - **格式验证**:验证项目中所有 `README.OpenSource` 文件的格式,检查必需字段是否完整,JSON 格式是否正确。 - - **内容验证**:对 `README.OpenSource` 文件中的特定字段,如 `"Name"`、`"License"`、`"Version Number"`、`"Upstream URL"`,与参考数据进行比对,确保其内容符合预期。还会验证 `"License File"` 字段指向的文件是否存在,路径应相对于 `README.OpenSource` 文件所在目录。 + - **内容验证**:验证以下内容: + - 对 `"Name"`、`"License"`、`"Version Number"`、`"Upstream URL"` 等字段与参考数据比对。 + - 验证 `"License File"` 字段指向的文件是否存在。 + - 验证 `"Dependencies"` 字段(若存在)是否为有效的字符串数组。 ## 安装与环境配置 @@ -88,196 +88,69 @@ ``` 3. **按照提示输入信息** - - - 脚本将提示您输入输出目录,默认为当前目录。 - - 输入每个部件的详细信息,包括: - - Name - - License - - License File - - Version Number - - Owner - - Upstream URL - - Description - - 输入完成后,选择是否添加另一个部件。 + - 输入输出目录(默认为当前目录) + - 依次输入每个部件的详细信息: + - Name:组件名称 + - Version Number:版本号 + - Owner:维护者 + - Upstream URL:上游地址 + - Description:描述信息 + - License:许可证(多个许可证用分号分隔) + - License File:许可证文件路径(多个文件用分号分隔) + - Dependencies(可选):依赖项(多个依赖用逗号分隔) + - 每个部件信息输入完成后,选择是否添加另一个部件 4. **完成生成** - - - 脚本将在指定的输出目录下生成 `README.OpenSource` 文件。 - -**示例:** - -``` -python generate_readme_opensource.py - -请输入输出目录(默认当前目录): -Name: elfutils -License: LGPL-2.1, LGPL-3.0, GPL-2.0 -License File: COPYING-GPLV2 -Version Number: 0.188 -Owner: opensource@sourceware.org -Upstream URL: https://sourceware.org/elfutils/ -Description: A collection of tools and libraries. -是否添加另一个部件?(y/n): y -Name: OpenSSL -License: Apache-2.0 -License File: LICENSE -Version Number: 1.1.1 -Owner: opensource@openssl.org -Upstream URL: https://www.openssl.org/ -Description: A toolkit for TLS and SSL protocols. -是否添加另一个部件?(y/n): n -已生成 ./README.OpenSource -``` + - 脚本将在指定的输出目录下生成JSON格式的 `README.OpenSource` 文件 ### 验证 `README.OpenSource` 文件 -运行 `validate_readme_opensource.py` 脚本,使用不同的参数进行格式或内容验证。 - #### 验证格式 验证项目中所有 `README.OpenSource` 文件的格式和必需字段。 -**步骤:** - -1. **进入项目目录** - - ``` - cd src - ``` - -2. **运行格式验证脚本** - - ``` - python validate_readme_opensource.py --validate-format [目录路径] - ``` - - - 如果不指定目录路径,默认验证当前目录。 - -3. **查看验证结果** - - - 脚本将输出验证结果,指示文件是否有效。 - - 如果有错误,脚本会列出具体的错误信息。 - -**示例:** +**命令:** +```bash +python validate_readme_opensource.py --validate-format [目录路径] ``` -python validate_readme_opensource.py --validate-format . -./README.OpenSource format is valid. -``` +验证检查内容: +- JSON格式的正确性 +- 必需字段的完整性 +- Dependencies字段(若存在)的数组格式 #### 验证内容 -对 `README.OpenSource` 文件中的特定字段与参考数据进行比对。 - -**步骤:** - -1. **准备参考数据** - - 创建一个包含参考数据的 JSON 文件,例如 `reference_data.json`,内容如下: - - ``` - [ - { - "Name": "elfutils", - "License": "LGPL-2.1, LGPL-3.0, GPL-2.0", - "Version Number": "0.188", - "Upstream URL": "https://sourceware.org/elfutils/" - }, - { - "Name": "OpenSSL", - "License": "Apache-2.0", - "Version Number": "1.1.1", - "Upstream URL": "https://www.openssl.org/" - } - ] - ``` - -2. **运行内容验证脚本** - - ``` - python validate_readme_opensource.py --validate-content --reference-data reference_data.json [目录路径] - ``` - -3. **查看验证结果** - - - 脚本将输出验证结果,指示文件是否有效。 - - 如果有错误,脚本会列出具体的错误信息。 +验证 `README.OpenSource` 文件内容与参考数据的一致性。 -**示例:** +**命令:** +```bash +python validate_readme_opensource.py --validate-content --reference-data reference_data.json [目录路径] ``` -python validate_readme_opensource.py --validate-content --reference-data reference_data.json . -Validating: ./README.OpenSource -./README.OpenSource: Content validation passed. -Validation process completed. -``` +验证内容包括: +- 核心字段(Name、License、Version Number、Upstream URL)与参考数据的一致性 +- License File 文件实际存在性检查 +- Dependencies字段(若存在)的有效性检查 ### 命令行参数 -- `--validate-format`:验证 `README.OpenSource` 文件的格式和必需字段。 -- `--validate-content`:验证 `README.OpenSource` 文件的内容,与参考数据比对。 -- `--reference-data`:指定参考数据的 JSON 文件路径(内容验证时必需)。 -- `--log-file`:指定日志文件路径,保存验证结果。 - -**注意**:`--validate-format` 和 `--validate-content` 可以组合使用,顺序执行格式和内容验证。 - -## 测试与验证 - -### 自动化测试 - -项目包含自动化测试用例,确保工具的可靠性。 - -#### 运行测试 - -1. **进入项目根目录** - - ``` - cd opensource_tools - ``` - -2. **运行所有测试用例** - - ``` - python -m unittest discover -s test - ``` +验证工具支持以下命令行参数: -#### 预期结果 - -``` -.. ----------------------------------------------------------------------- -Ran 3 tests in 0.001s - -OK -``` - -### 手动测试 - -#### 生成工具测试 - -- **测试正常输入**:按照使用指南运行生成脚本,输入多个部件的信息,检查生成的 `README.OpenSource` 文件内容是否正确。 -- **测试边界情况**:输入空值、特殊字符、超长字符串,观察脚本是否能正常处理。 - -#### 验证工具测试 - -- **验证正确的文件**:使用生成的正确的 `README.OpenSource` 文件,运行验证脚本,确保验证通过。 -- **验证错误的文件**:手动修改 `README.OpenSource` 文件,引入格式错误或缺少字段,运行验证脚本,检查是否能正确捕获错误。 -- **验证内容不匹配**:修改 `README.OpenSource` 文件的字段,使其与参考数据不一致,运行内容验证,检查是否能正确报告不匹配项。 +- `project_root`:必需,项目根目录路径 +- `--validate-format`:执行格式验证 +- `--validate-content`:执行内容验证 +- `--reference-data`:参考数据JSON文件路径(内容验证必需) +- `--log-file`:日志文件路径,用于保存验证结果 ## 目录结构 -以下是项目的目录结构: - - **src/** - - `generate_readme_opensource.py`:生成README.OpenSource开源不见配置信息脚本 - - `validate_readme_opensource.py`:验证工具脚本,包含JSON格式验证和内容验证功能。 - -- **test/** - - `test_generate_readme_opensource.py`:生成工具测试用例 - - `test_validate_readme_opensource.py`:工具的测试用例。 -- **README_OSS.md**:使用文档(本文件)。 + - `generate_readme_opensource.py`:生成README.OpenSource开源部件配置信息脚本 + - `validate_readme_opensource.py`:验证工具脚本,包含JSON格式验证和内容验证功能 +- **README_OSS.md**:使用文档(本文件) ## 工作流程概览 @@ -286,18 +159,26 @@ OK ```mermaid sequenceDiagram participant User as 用户 - participant Script as validate_readme_opensource.py - User->>Script: 运行脚本 (--generate) - Script-->>User: 请输入输出目录 - User->>Script: 输入输出目录 + participant Script as generate_readme_opensource.py + participant FileSystem as 文件系统 + + User->>Script: 运行脚本 + Script-->>User: 请求输出目录 + User->>Script: 输入目录路径 + loop 添加开源部件 - Script-->>User: 请输入部件信息 - User->>Script: 输入开源部件信息 - Script-->>User: 是否添加另一个开源部件?(y/n) - User->>Script: y 或 n + Script-->>User: 请求基本信息 (Name等) + User->>Script: 输入基本信息 + Script-->>User: 请求许可证信息 + User->>Script: 输入许可证及文件路径 + Script-->>User: 请求依赖项信息(可选) + User->>Script: 输入依赖项 + Script-->>User: 是否继续添加?(y/n) + User->>Script: 选择是否继续 end - Script->>FileSystem: 生成 README.OpenSource 文件 - Script-->>User: 已生成 README.OpenSource + + Script->>FileSystem: 生成 README.OpenSource + Script-->>User: 完成生成 ``` ### 验证工具流程图 @@ -306,16 +187,26 @@ sequenceDiagram sequenceDiagram participant User as 用户 participant Script as validate_readme_opensource.py - User->>Script: 运行脚本 (--validate-format/--validate-content) + participant FileSystem as 文件系统 + + User->>Script: 运行验证命令 Script->>FileSystem: 搜索 README.OpenSource 文件 - alt 找到文件 - Script->>Script: 验证文件内容 - alt 验证通过 - Script-->>User: 文件有效 - else 验证失败 - Script-->>User: 文件无效,输出错误信息 - end - else 未找到文件 - Script-->>User: 未找到 README.OpenSource 文件 + + alt 格式验证 + Script->>Script: 检查JSON格式 + Script->>Script: 检查必需字段 + Script->>Script: 检查Dependencies格式 end -``` + + alt 内容验证 + Script->>Script: 加载参考数据 + Script->>Script: 比对字段内容 + Script->>FileSystem: 检查License文件 + Script->>Script: 验证Dependencies + end + + alt 验证通过 + Script-->>User: 报告成功 + else 验证失败 + Script-->>User: 输出错误信息 + end \ No newline at end of file diff --git a/tools/opensource_tools/src/generate_readme_opensource.py b/tools/opensource_tools/src/generate_readme_opensource.py index 8d4cdfc..669181c 100644 --- a/tools/opensource_tools/src/generate_readme_opensource.py +++ b/tools/opensource_tools/src/generate_readme_opensource.py @@ -1,8 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os import json -def ask_question(prompt): - return input(prompt).strip() +def ask_question(prompt, default_value=None): + """ 提示用户输入,若没有输入则使用默认值 """ + value = input(f"{prompt} [{default_value}]: ").strip() + return value or default_value + +def ask_for_list(prompt): + """ 提示用户输入一个列表,以逗号分隔 """ + value = input(f"{prompt} (多个项请用逗号分隔): ").strip() + return [item.strip() for item in value.split(',')] if value else [] + +# def process_license_info(): +# """ 处理许可证信息和对应的文件路径 """ +# licenses = ask_question("请输入许可证名称(如有多个,用分号分隔)") +# license_files = ask_question("请输入许可证文件路径(如果有多个,请使用分号分隔)") +# +# license_list = [license.strip() for license in licenses.split(';')] +# license_file_list = [file.strip() for file in license_files.split(';')] +# +# if len(license_list) == len(license_file_list): +# return license_list, license_file_list +# elif len(license_list) == 1 and len(license_file_list) > 1: +# return license_list, license_file_list +# elif len(license_list) > 1 and len(license_file_list) == 1: +# return license_list, license_file_list +# else: +# raise ValueError("许可证和许可证文件的数量不匹配,或者格式错误。") + +def process_license_info(): + """ 处理许可证信息和对应的文件路径 """ + licenses = ask_question("请输入许可证名称(如有多个,用分号分隔)") + license_files = ask_question("请输入许可证文件路径(如果有多个,请使用分号分隔)") + + license_list = [license.strip() for license in licenses.split(';')] if licenses else [] + license_file_list = [file.strip() for file in license_files.split(';')] if license_files else [] + + # 检查输入是否为空 + if not license_list or not license_file_list: + raise ValueError("许可证和许可证文件路径不能为空。") + + # 检查许可证和文件路径的匹配情况 + if len(license_list) != len(license_file_list): + # 只有在以下两种特殊情况下允许不相等: + # 1. 一个许可证对应多个文件 + # 2. 多个许可证对应一个文件 + if not ((len(license_list) == 1 and len(license_file_list) > 1) or + (len(license_list) > 1 and len(license_file_list) == 1)): + raise ValueError("许可证和许可证文件的数量不匹配,必须是一对一、一对多或多对一的关系。") + + return license_list, license_file_list def generate_readme_opensource(output_dir): """ @@ -11,8 +73,6 @@ def generate_readme_opensource(output_dir): components = [] fields = [ "Name", - "License", - "License File", "Version Number", "Owner", "Upstream URL", @@ -22,18 +82,34 @@ def generate_readme_opensource(output_dir): print("请输入开源组件的信息(输入完成后,可选择继续添加另一个组件):") while True: component = {} + # 获取组件的基本信息 for field in fields: value = ask_question(f"{field}: ") component[field] = value + + # 获取许可证信息 + license_list, license_file_list = process_license_info() + component["License"] = "; ".join(license_list) + component["License File"] = "; ".join(license_file_list) + + # 获取依赖信息(可选) + dependencies = ask_for_list("请输入该软件的依赖项(如果有多个,请用逗号分隔)") + if dependencies: + component["Dependencies"] = dependencies + + # 将组件信息添加到列表 components.append(component) + # 是否继续添加组件 add_more = ask_question("是否添加另一个组件?(y/n): ").lower() if add_more != 'y': break + # 确保输出目录存在 if not os.path.exists(output_dir): os.makedirs(output_dir) + # 输出 README.OpenSource 文件 readme_path = os.path.join(output_dir, 'README.OpenSource') with open(readme_path, 'w', encoding='utf-8') as f: json.dump(components, f, indent=2, ensure_ascii=False) diff --git a/tools/opensource_tools/src/spdx_license_matcher.py b/tools/opensource_tools/src/spdx_license_matcher.py index 38b29bc..852a346 100644 --- a/tools/opensource_tools/src/spdx_license_matcher.py +++ b/tools/opensource_tools/src/spdx_license_matcher.py @@ -1,3 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import re import sys import json diff --git a/tools/opensource_tools/src/validate_readme_opensource.py b/tools/opensource_tools/src/validate_readme_opensource.py index 9bbc82e..8587d7a 100644 --- a/tools/opensource_tools/src/validate_readme_opensource.py +++ b/tools/opensource_tools/src/validate_readme_opensource.py @@ -1,3 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os import json import argparse @@ -14,12 +29,13 @@ REQUIRED_FIELDS = [ "Description" ] + class OpenSourceValidator: def __init__( - self, - project_root: str, - log_file: Optional[str] = None, - reference_data: Optional[List[Dict[str, str]]] = None + self, + project_root: str, + log_file: Optional[str] = None, + reference_data: Optional[List[Dict[str, str]]] = None ): self.project_root = project_root self.reference_data = reference_data or [] @@ -59,6 +75,16 @@ class OpenSourceValidator: for field in REQUIRED_FIELDS: if field not in component: errors.append(f"Component {idx + 1} is missing required field: {field}") + + # 校验 Dependencies 字段是否存在并且是一个数组 + if "Dependencies" in component: + if not isinstance(component["Dependencies"], list): + errors.append(f"Component {idx + 1} 'Dependencies' field must be an array.") + else: + for dep in component["Dependencies"]: + if not isinstance(dep, str): + errors.append(f"Component {idx + 1} 'Dependencies' contains a non-string value: {dep}") + except json.JSONDecodeError as e: errors.append(f"JSON decode error: {e}") return False @@ -81,7 +107,7 @@ class OpenSourceValidator: self.reference_data = json.load(f) except Exception as e: raise ValueError( - f"Failed to load reference data from {reference_data_path}: {e}" + f"Failed to load reference data from '{reference_data_path}': {e}" ) def find_reference_data(self, name: str) -> Optional[Dict[str, str]]: @@ -136,6 +162,10 @@ class OpenSourceValidator: if not self.validate_license_file(readme_path, software_data.get("License File")): all_valid = False + # 校验依赖项(Dependencies)是否正确 + if not self.validate_dependencies(software_data.get("Dependencies"), readme_path): + all_valid = False + if all_valid: logging.info(f"{readme_path}: Content validation passed.") else: @@ -148,17 +178,39 @@ class OpenSourceValidator: logging.error(f"{readme_path}: 'License File' field is missing.") return False + # 支持多个许可证文件路径,以分号分隔 + license_files = license_file.split(';') readme_dir = os.path.dirname(readme_path) - license_file_path = os.path.join(readme_dir, license_file) + all_valid = True - if not os.path.exists(license_file_path): - logging.error( - f"{readme_path}: License file '{license_file}' not found at: {license_file_path}" - ) + for file in license_files: + license_file_path = os.path.join(readme_dir, file.strip()) + if not os.path.exists(license_file_path): + logging.error( + f"{readme_path}: License file '{file.strip()}' not found at: {license_file_path}" + ) + all_valid = False + else: + logging.info(f"{readme_path}: License file '{file.strip()}' exists.") + + return all_valid + + def validate_dependencies(self, dependencies: Optional[List[str]], readme_path: str) -> bool: + """校验 Dependencies 字段是否符合预期""" + if dependencies is None: + return True # 没有依赖项是合法的 + + if not isinstance(dependencies, list): + logging.error(f"{readme_path}: 'Dependencies' should be an array.") return False - else: - logging.info(f"{readme_path}: License file '{license_file}' exists.") - return True + + for dep in dependencies: + if not isinstance(dep, str): + logging.error(f"{readme_path}: 'Dependencies' contains non-string value: {dep}") + return False + + logging.info(f"{readme_path}: 'Dependencies' field is valid.") + return True def run_validation(self, validate_format: bool = True, validate_content: bool = False): """运行完整的校验流程,递归处理所有 README.OpenSource 文件""" @@ -217,11 +269,10 @@ def main(): # 执行校验流程 validator.run_validation( - validate_format=args.validate_format or not (args.validate_format or args.validate_content), + validate_format=args.validate_format, validate_content=args.validate_content ) if __name__ == "__main__": main() - diff --git a/tools/opensource_tools/test/test_generate_readme_opensource.py b/tools/opensource_tools/test/test_generate_readme_opensource.py index 4b5f76b..f83ae07 100644 --- a/tools/opensource_tools/test/test_generate_readme_opensource.py +++ b/tools/opensource_tools/test/test_generate_readme_opensource.py @@ -1,70 +1,180 @@ -import unittest +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest import os import json -import shutil from unittest.mock import patch -from src.generate_readme_opensource import generate_readme_opensource - -class TestGenerateReadmeOpenSource(unittest.TestCase): - - def setUp(self): - self.test_output_dir = 'test_output' - if not os.path.exists(self.test_output_dir): - os.makedirs(self.test_output_dir) - - def tearDown(self): - if os.path.exists(self.test_output_dir): - shutil.rmtree(self.test_output_dir) - - @patch('builtins.input', side_effect=[ - # First component - 'elfutils', # Name - 'LGPL-2.1, LGPL-3.0, GPL-2.0', # License - 'COPYING-GPLV2', # License File - '0.188', # Version Number - 'zhanghaibo0@huawei.com', # Owner - 'https://sourceware.org/elfutils/', # Upstream URL - 'A collection of tools and libraries.', # Description - 'y', # Add another component - # Second component - 'OpenSSL', # Name - 'Apache-2.0', # License +import sys + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) +from generate_readme_opensource import ( + ask_question, + ask_for_list, + process_license_info, + generate_readme_opensource +) + + +@pytest.fixture +def temp_output_dir(tmp_path): + """创建临时输出目录""" + return str(tmp_path) + + +def test_ask_question(): + """测试ask_question函数""" + with patch('builtins.input', return_value='test_value'): + assert ask_question("prompt", "default") == "test_value" + + with patch('builtins.input', return_value=''): + assert ask_question("prompt", "default") == "default" + + +def test_ask_for_list(): + """测试ask_for_list函数""" + with patch('builtins.input', return_value='item1, item2, item3'): + result = ask_for_list("prompt") + assert result == ['item1', 'item2', 'item3'] + + with patch('builtins.input', return_value=''): + result = ask_for_list("prompt") + assert result == [] + + +def test_process_license_info_single(): + """测试处理单个许可证信息""" + with patch('builtins.input', side_effect=['MIT', 'LICENSE']): + licenses, files = process_license_info() + assert licenses == ['MIT'] + assert files == ['LICENSE'] + + +def test_process_license_info_multiple(): + """测试处理多个许可证信息""" + with patch('builtins.input', side_effect=['MIT; Apache-2.0', 'LICENSE.mit; LICENSE.apache']): + licenses, files = process_license_info() + assert licenses == ['MIT', 'Apache-2.0'] + assert files == ['LICENSE.mit', 'LICENSE.apache'] + + +def test_process_license_info_one_license_multiple_files(): + """测试一个许可证对应多个文件的情况""" + with patch('builtins.input', side_effect=['MIT', 'LICENSE.txt; COPYING.txt']): + licenses, files = process_license_info() + assert licenses == ['MIT'] + assert files == ['LICENSE.txt', 'COPYING.txt'] + + +def test_process_license_info_multiple_licenses_one_file(): + """测试多个许可证对应一个文件的情况""" + with patch('builtins.input', side_effect=['MIT; Apache-2.0', 'LICENSE']): + licenses, files = process_license_info() + assert licenses == ['MIT', 'Apache-2.0'] + assert files == ['LICENSE'] + + +def test_process_license_info_error(): + """测试许可证信息不匹配的错误情况""" + # 测试三个许可证对应两个文件的情况,这应该会引发错误 + with patch('builtins.input', side_effect=['MIT; Apache-2.0; GPL', 'LICENSE.mit; LICENSE.apache']): + with pytest.raises(ValueError) as exc_info: + process_license_info() + assert "许可证和许可证文件的数量不匹配" in str(exc_info.value) + + +def test_generate_readme_opensource(temp_output_dir): + """测试生成README.OpenSource文件""" + # 模拟用户输入 + input_values = [ + 'TestComponent', # Name + '1.0.0', # Version + 'Test Owner', # Owner + 'https://example.com', # URL + 'Test Description', # Description + 'MIT', # License 'LICENSE', # License File - '1.1.1', # Version Number - 'opensource@openssl.org', # Owner - 'https://www.openssl.org/', # Upstream URL - 'A robust, commercial-grade, full-featured toolkit for the TLS and SSL protocols.', # Description - 'n' # Do not add another component - ]) - def test_generate_readme_opensource(self, mock_inputs): - generate_readme_opensource(self.test_output_dir) - readme_path = os.path.join(self.test_output_dir, 'README.OpenSource') - self.assertTrue(os.path.exists(readme_path)) + 'dep1, dep2', # Dependencies + 'n' # Don't add more components + ] + + with patch('builtins.input', side_effect=input_values): + generate_readme_opensource(temp_output_dir) + + # 验证文件是否创建 + readme_path = os.path.join(temp_output_dir, 'README.OpenSource') + assert os.path.exists(readme_path) + # 验证文件内容 with open(readme_path, 'r', encoding='utf-8') as f: - data = json.load(f) - expected_data = [ - { - "Name": "elfutils", - "License": "LGPL-2.1, LGPL-3.0, GPL-2.0", - "License File": "COPYING-GPLV2", - "Version Number": "0.188", - "Owner": "zhanghaibo0@huawei.com", - "Upstream URL": "https://sourceware.org/elfutils/", - "Description": "A collection of tools and libraries." - }, - { - "Name": "OpenSSL", - "License": "Apache-2.0", - "License File": "LICENSE", - "Version Number": "1.1.1", - "Owner": "opensource@openssl.org", - "Upstream URL": "https://www.openssl.org/", - "Description": "A robust, commercial-grade, full-featured toolkit for the TLS and SSL protocols." - } - ] - self.assertEqual(data, expected_data) - -if __name__ == '__main__': - unittest.main() + content = json.load(f) + assert len(content) == 1 + component = content[0] + assert component['Name'] == 'TestComponent' + assert component['Version Number'] == '1.0.0' + assert component['Owner'] == 'Test Owner' + assert component['Upstream URL'] == 'https://example.com' + assert component['Description'] == 'Test Description' + assert component['License'] == 'MIT' + assert component['License File'] == 'LICENSE' + assert component['Dependencies'] == ['dep1', 'dep2'] + + +def test_generate_readme_opensource_multiple_components(temp_output_dir): + """测试生成包含多个组件的README.OpenSource文件""" + input_values = [ + # 第一个组件 + 'Component1', + '1.0.0', + 'Owner1', + 'https://example1.com', + 'Description1', + 'MIT', + 'LICENSE1', + '', # 无依赖项 + 'y', # 添加另一个组件 + # 第二个组件 + 'Component2', + '2.0.0', + 'Owner2', + 'https://example2.com', + 'Description2', + 'Apache-2.0', + 'LICENSE2', + 'dep1', + 'n' # 不再添加组件 + ] + + with patch('builtins.input', side_effect=input_values): + generate_readme_opensource(temp_output_dir) + + # 验证文件是否创建 + readme_path = os.path.join(temp_output_dir, 'README.OpenSource') + assert os.path.exists(readme_path) + + # 验证文件内容 + with open(readme_path, 'r', encoding='utf-8') as f: + content = json.load(f) + assert len(content) == 2 + + # 验证第一个组件 + assert content[0]['Name'] == 'Component1' + assert content[0]['Version Number'] == '1.0.0' + assert 'Dependencies' not in content[0] + # 验证第二个组件 + assert content[1]['Name'] == 'Component2' + assert content[1]['Version Number'] == '2.0.0' + assert content[1]['Dependencies'] == ['dep1'] \ No newline at end of file diff --git a/tools/opensource_tools/test/test_spdx_license_matcher.py b/tools/opensource_tools/test/test_spdx_license_matcher.py index 84c14e2..da93eb2 100644 --- a/tools/opensource_tools/test/test_spdx_license_matcher.py +++ b/tools/opensource_tools/test/test_spdx_license_matcher.py @@ -1,3 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import unittest import pandas as pd import json diff --git a/tools/opensource_tools/test/test_validate_readme_opensource.py b/tools/opensource_tools/test/test_validate_readme_opensource.py index 69f69d2..40030fc 100644 --- a/tools/opensource_tools/test/test_validate_readme_opensource.py +++ b/tools/opensource_tools/test/test_validate_readme_opensource.py @@ -1,168 +1,235 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import unittest -from unittest.mock import patch, mock_open, MagicMock -import json +from unittest.mock import patch, MagicMock import os - -# 确保导入 OpenSourceValidator 类 -from src.validate_readme_opensource import OpenSourceValidator +import json +from src.validate_readme_opensource import OpenSourceValidator, REQUIRED_FIELDS class TestOpenSourceValidator(unittest.TestCase): - def setUp(self): - self.project_root = "/fake/project/root" - self.validator = OpenSourceValidator(self.project_root) - self.readme_content_valid = json.dumps( - [ - { - "Name": "TestLibrary", - "License": "MIT", - "License File": "LICENSE", - "Version Number": "1.0.0", - "Owner": "TestOwner", - "Upstream URL": "http://example.com", - "Description": "A test library.", - } - ] - ) - self.readme_content_missing_fields = json.dumps( - [ - { - "Name": "TestLibrary", - # Missing 'License' - "License File": "LICENSE", - "Version Number": "1.0.0", - "Owner": "TestOwner", - "Upstream URL": "http://example.com", - "Description": "A test library.", - } - ] - ) - self.invalid_json_content = "{ invalid json }" - self.reference_data = [ + + @patch("os.walk") + def test_find_all_readmes(self, mock_os_walk): + # 模拟 os.walk 返回值 + mock_os_walk.return_value = [ + ("/project", ["subdir1", "subdir2"], ["README.OpenSource"]), + ("/project/subdir1", [], ["README.OpenSource"]), + ("/project/subdir2", [], ["README.OpenSource"]), + ] + + validator = OpenSourceValidator(project_root="/project") + readme_paths = validator.find_all_readmes() + + # 断言所有的 README.OpenSource 文件都被正确地找到 + self.assertEqual(readme_paths, [ + "/project/README.OpenSource", + "/project/subdir1/README.OpenSource", + "/project/subdir2/README.OpenSource" + ]) + + @patch("builtins.open", new_callable=MagicMock) + def test_validate_format_valid(self, mock_open): + # 模拟文件内容是一个包含正确格式的 JSON 数据 + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([ + { + "Name": "Software A", + "License": "MIT", + "License File": "LICENSE", + "Version Number": "1.0.0", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + "Description": "A software project", + "Dependencies": [] + } + ]) + + validator = OpenSourceValidator(project_root="/project") + valid = validator.validate_format("/project/README.OpenSource") + + # 断言格式验证通过 + self.assertTrue(valid) + + @patch("builtins.open", new_callable=MagicMock) + def test_validate_format_invalid_missing_field(self, mock_open): + # 模拟文件内容是一个包含缺失字段的 JSON 数据 + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([ + { + "Name": "Software A", + "License": "MIT", + "License File": "LICENSE", + "Version Number": "1.0.0", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + # "Description" 字段缺失 + "Dependencies": [] + } + ]) + + validator = OpenSourceValidator(project_root="/project") + valid = validator.validate_format("/project/README.OpenSource") + + # 断言格式验证失败 + self.assertFalse(valid) + + @patch("builtins.open", new_callable=MagicMock) + def test_validate_content_valid(self, mock_open): + # 模拟读取到的 README 文件数据 + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([ { - "Name": "TestLibrary", + "Name": "Software A", "License": "MIT", + "License File": "LICENSE", "Version Number": "1.0.0", - "Upstream URL": "http://example.com", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + "Description": "A software project", + "Dependencies": [] + } + ]) + + # 模拟参考数据 + reference_data = [ + { + "Name": "Software A", + "License": "MIT", + "License File": "LICENSE", + "Version Number": "1.0.0", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + "Description": "A software project" } ] - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open, read_data="") - def test_validate_format_valid(self, mock_file, mock_os_walk): - # 模拟 os.walk 返回一个 README.OpenSource 文件 - mock_os_walk.return_value = [(self.project_root, [], ["README.OpenSource"])] - # 模拟打开文件并返回有效内容 - mock_file.return_value.read.return_value = self.readme_content_valid + validator = OpenSourceValidator(project_root="/project") + validator.reference_data = reference_data # 设置参考数据 - self.validator.run_validation(validate_format=True, validate_content=False) - # 如果没有异常,则测试通过 + valid = validator.validate_content("/project/README.OpenSource") - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open, read_data="") - def test_validate_format_missing_fields(self, mock_file, mock_os_walk): - mock_os_walk.return_value = [(self.project_root, [], ["README.OpenSource"])] - mock_file.return_value.read.return_value = self.readme_content_missing_fields + # 断言内容验证通过 + self.assertTrue(valid) + + @patch("builtins.open", new_callable=MagicMock) + @patch("os.path.exists", return_value=True) # 模拟许可证文件存在 + def test_validate_content_valid(self, mock_exists, mock_open): + # 模拟读取到的 README 文件数据 + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([ + { + "Name": "Software A", + "License": "MIT", + "License File": "LICENSE", # 许可证文件 + "Version Number": "1.0.0", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + "Description": "A software project", + "Dependencies": [] + } + ]) + + # 模拟参考数据 + reference_data = [ + { + "Name": "Software A", + "License": "MIT", + "License File": "LICENSE", + "Version Number": "1.0.0", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + "Description": "A software project" + } + ] - self.validator.run_validation(validate_format=True, validate_content=False) - # 检查日志或假设如果出现异常,测试将失败 + validator = OpenSourceValidator(project_root="/project") + validator.reference_data = reference_data # 设置参考数据 - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open, read_data="") - def test_validate_format_invalid_json(self, mock_file, mock_os_walk): - mock_os_walk.return_value = [(self.project_root, [], ["README.OpenSource"])] - mock_file.return_value.read.return_value = self.invalid_json_content + valid = validator.validate_content("/project/README.OpenSource") - self.validator.run_validation(validate_format=True, validate_content=False) - # 检查是否正确处理 JSON 解码错误 + # 断言内容验证通过 + self.assertTrue(valid) - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - def test_validate_content_valid(self, mock_open_builtin, mock_os_walk): - # 模拟 os.walk 返回一个 README.OpenSource 文件 - mock_os_walk.return_value = [(self.project_root, [], ["README.OpenSource"])] - - # 模拟打开文件并返回有效内容 - def side_effect_open(file, mode="r", encoding=None): - if "README.OpenSource" in file: - return mock_open(read_data=self.readme_content_valid)() - elif "reference_data.json" in file: - return mock_open(read_data=json.dumps(self.reference_data))() - else: - raise FileNotFoundError - - mock_open_builtin.side_effect = side_effect_open - - # 初始化验证器并加载参考数据 - self.validator.reference_data = self.reference_data - - # 模拟 os.path.exists 返回 True(表示许可证文件存在) - with patch("os.path.exists", return_value=True): - self.validator.run_validation(validate_format=False, validate_content=True) - # 如果没有异常,则测试通过 + @patch("os.path.exists") + def test_validate_license_file_valid(self, mock_exists): + # 模拟文件存在 + mock_exists.return_value = True - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - def test_validate_content_license_file_missing( - self, mock_open_builtin, mock_os_walk - ): - # 与上一个测试类似 - mock_os_walk.return_value = [(self.project_root, [], ["README.OpenSource"])] - - def side_effect_open(file, mode="r", encoding=None): - if "README.OpenSource" in file: - return mock_open(read_data=self.readme_content_valid)() - elif "reference_data.json" in file: - return mock_open(read_data=json.dumps(self.reference_data))() - else: - raise FileNotFoundError - - mock_open_builtin.side_effect = side_effect_open - - self.validator.reference_data = self.reference_data - - # 模拟 os.path.exists 返回 False(表示许可证文件不存在) - with patch("os.path.exists", return_value=False): - self.validator.run_validation(validate_format=False, validate_content=True) - # 检查是否正确处理许可证文件缺失 + validator = OpenSourceValidator(project_root="/project") + valid = validator.validate_license_file("/project/README.OpenSource", "LICENSE") - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - def test_validate_content_field_mismatch(self, mock_open_builtin, mock_os_walk): - # 修改 README 内容以包含不匹配的字段 - readme_content_mismatch = json.dumps( - [ - { - "Name": "TestLibrary", - "License": "Apache-2.0", # 应为 'MIT' - "License File": "LICENSE", - "Version Number": "1.0.0", - "Owner": "TestOwner", - "Upstream URL": "http://example.com", - "Description": "A test library.", - } - ] - ) - - mock_os_walk.return_value = [(self.project_root, [], ["README.OpenSource"])] - - def side_effect_open(file, mode="r", encoding=None): - if "README.OpenSource" in file: - return mock_open(read_data=readme_content_mismatch)() - elif "reference_data.json" in file: - return mock_open(read_data=json.dumps(self.reference_data))() - else: - raise FileNotFoundError - - mock_open_builtin.side_effect = side_effect_open - - self.validator.reference_data = self.reference_data - - # 模拟 os.path.exists 返回 True - with patch("os.path.exists", return_value=True): - self.validator.run_validation(validate_format=False, validate_content=True) - # 检查是否正确处理字段不匹配 + # 断言许可证文件存在并且校验通过 + self.assertTrue(valid) + + @patch("os.path.exists") + def test_validate_license_file_invalid(self, mock_exists): + # 模拟文件不存在 + mock_exists.return_value = False + + validator = OpenSourceValidator(project_root="/project") + valid = validator.validate_license_file("/project/README.OpenSource", "LICENSE") + + # 断言许可证文件不存在并且校验失败 + self.assertFalse(valid) + + @patch("builtins.open", new_callable=MagicMock) + @patch("os.path.exists") + def test_validate_dependencies_valid(self, mock_exists, mock_open): + # 模拟文件内容是有效的 JSON 数据 + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([ + { + "Name": "Software A", + "License": "MIT", + "License File": "LICENSE", + "Version Number": "1.0.0", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + "Description": "A software project", + "Dependencies": ["dep1", "dep2"] + } + ]) + # 模拟依赖文件存在 + mock_exists.return_value = True + + validator = OpenSourceValidator(project_root="/project") + valid = validator.validate_dependencies(["dep1", "dep2"], "/project/README.OpenSource") + + # 断言依赖项验证通过 + self.assertTrue(valid) + + @patch("builtins.open", new_callable=MagicMock) + def test_validate_dependencies_invalid(self, mock_open): + # 模拟文件内容是包含非法依赖项格式的 JSON 数据 + mock_open.return_value.__enter__.return_value.read.return_value = json.dumps([ + { + "Name": "Software A", + "License": "MIT", + "License File": "LICENSE", + "Version Number": "1.0.0", + "Owner": "Owner A", + "Upstream URL": "https://example.com", + "Description": "A software project", + "Dependencies": ["dep1", 123] # 非字符串依赖项 + } + ]) + + validator = OpenSourceValidator(project_root="/project") + valid = validator.validate_dependencies(["dep1", 123], "/project/README.OpenSource") + + # 断言依赖项验证失败 + self.assertFalse(valid) if __name__ == "__main__": unittest.main() + -- Gitee