Skip to content

Commit d750062

Browse files
committed
use python script to replace part of shell script
1 parent d6fcc2e commit d750062

File tree

5 files changed

+194
-39
lines changed

5 files changed

+194
-39
lines changed

.github/workflows/test.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Run Tests
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.13"
20+
21+
- name: Run tests
22+
run: |
23+
python -m unittest tests.test_copy_files

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.vscode/
2+
__pycache__/

action.yml

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -102,42 +102,10 @@ runs:
102102
rm -rf pystand
103103
104104
- name: Copy Sources and Assets to Build directory
105-
shell: bash
106-
run: |
107-
set -e
108-
build_path="${{ steps.init-build-directory.outputs.build-directory }}"
109-
110-
# resolves multi-line input of GitHub Actions
111-
process_input() {
112-
local input="$1"
113-
# remove return symbol, split by line, trim spaces
114-
echo "$input" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | grep -v '^$'
115-
}
116-
117-
ITEMS=()
118-
while IFS= read -r line; do
119-
[[ -n "$line" ]] && ITEMS+=("$line")
120-
done < <(process_input "${{ inputs.included-files }}")
121-
122-
echo "=== Processed Items ==="
123-
printf 'Item: "%s"\n' "${ITEMS[@]}"
124-
echo "=== End Processed Items ==="
125-
126-
for item in "${ITEMS[@]}"; do
127-
echo "Processing: $item"
128-
129-
if [ ! -e "$item" ]; then
130-
echo "::warning file=$item::Item does not exist, skipped."
131-
continue
132-
fi
133-
134-
if [ -d "$item" ]; then
135-
cp -rv "$item" "$build_path/"
136-
elif [ -f "$item" ]; then
137-
cp -v -- "$item" "$build_path/"
138-
else
139-
echo "::warning file=$item::Item is not a regular file or directory, skipped."
140-
fi
141-
done
142-
143-
cp "${{ inputs.pystand-entry-file }}" "$build_path/${{ inputs.application-name }}.py"
105+
id: copy-sources
106+
run: python scripts/copy_files.py
107+
env:
108+
BUILD_PATH: ${{ steps.init-build-directory.outputs.build-directory }}
109+
INCLUDED_FILES: ${{ inputs.included-files }}
110+
PYSTAND_ENTRY_FILE: ${{ inputs.pystand-entry-file }}
111+
APPLICATION_NAME: ${{ inputs.application-name }}

scripts/copy_files.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import os
2+
import sys
3+
import shutil
4+
5+
def process_input(input_str: str) -> list[str]:
6+
"""
7+
Resolves the multi-line inputs from GitHub Actions
8+
"""
9+
if not input_str:
10+
return []
11+
# filters empty lines, strips the spaces for every lines
12+
return [line.strip() for line in input_str.splitlines() if line.strip()]
13+
14+
def main():
15+
try:
16+
build_path = os.environ['BUILD_PATH']
17+
included_files_str = os.environ.get('INCLUDED_FILES', '')
18+
entry_file = os.environ['PYSTAND_ENTRY_FILE']
19+
app_name = os.environ['APPLICATION_NAME']
20+
except KeyError as e:
21+
print("Environment variable missing: ", e)
22+
sys.exit(1)
23+
24+
items_to_copy = process_input(included_files_str)
25+
26+
print("=== Processed Items ===")
27+
for item in items_to_copy:
28+
print(f'Item: "{item}"')
29+
print("=== End Processed Items ===")
30+
31+
try:
32+
for item in items_to_copy:
33+
print(f"Processing: {item}")
34+
if not os.path.exists(item):
35+
print(f"::warning file={item}::Item does not exist, skipped.")
36+
continue
37+
38+
if os.path.isdir(item):
39+
# Copies the source directory 'item' to the directory under the build dir with the name name.
40+
# For example, copies 'src/assets' to 'build_path/assets/'
41+
target_dir_path = os.path.join(build_path, os.path.basename(item))
42+
print(f"Copying directory '{item}' to '{target_dir_path}'")
43+
shutil.copytree(item, target_dir_path, dirs_exist_ok=True)
44+
elif os.path.isfile(item):
45+
print(f"Copying file '{item}' to '{build_path}'")
46+
shutil.copy2(item, build_path)
47+
else:
48+
print(f"::warning file={item}::Item is not a regular file or directory, skipped.")
49+
50+
# copy and rename PyStand entry file
51+
entry_dest = os.path.join(build_path, f"{app_name}.py")
52+
print(f"Copying entry file '{entry_file}' to '{entry_dest}'")
53+
shutil.copy2(entry_file, entry_dest)
54+
55+
print("=== All items processed successfully! ===")
56+
except Exception as e:
57+
print(f"::error::An unexpected error occurred: {e}")
58+
sys.exit(1)
59+
60+
if __name__ == "__main__":
61+
main()

tests/test_copy_files.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import unittest
2+
import os
3+
import shutil
4+
import tempfile
5+
import sys
6+
from unittest.mock import patch, ANY
7+
8+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'scripts')))
9+
from copy_files import process_input, main
10+
11+
class TestCopyFiles(unittest.TestCase):
12+
13+
def setUp(self):
14+
# 创建一个临时目录作为测试环境
15+
self.test_dir = tempfile.mkdtemp()
16+
self.build_dir = os.path.join(self.test_dir, 'build')
17+
os.makedirs(self.build_dir)
18+
self.source_dir = os.path.join(self.test_dir, 'source')
19+
os.makedirs(self.source_dir)
20+
21+
# 创建一些测试文件和目录
22+
self.test_file1 = os.path.join(self.source_dir, 'file1.txt')
23+
self.test_file2 = os.path.join(self.source_dir, 'file2.py')
24+
self.test_subdir = os.path.join(self.source_dir, 'subdir')
25+
os.makedirs(self.test_subdir)
26+
self.test_subfile = os.path.join(self.test_subdir, 'subfile.txt')
27+
28+
with open(self.test_file1, 'w') as f:
29+
f.write('This is file1.')
30+
with open(self.test_file2, 'w') as f:
31+
f.write('print("This is file2.")')
32+
with open(self.test_subfile, 'w') as f:
33+
f.write('This is a subfile.')
34+
35+
def tearDown(self):
36+
# 清理临时目录
37+
shutil.rmtree(self.test_dir)
38+
39+
def test_process_input_empty(self):
40+
self.assertEqual(process_input(''), [])
41+
42+
def test_process_input_single_line(self):
43+
self.assertEqual(process_input('file.txt'), ['file.txt'])
44+
45+
def test_process_input_multiple_lines(self):
46+
input_str = 'file1.txt\nfile2.py\n spaced_file.txt \n\n'
47+
expected = ['file1.txt', 'file2.py', 'spaced_file.txt']
48+
self.assertEqual(process_input(input_str), expected)
49+
50+
@patch('sys.exit')
51+
@patch('builtins.print')
52+
def test_main_missing_env_var(self, mock_print, mock_exit):
53+
with patch.dict(os.environ, {}, clear=True):
54+
main()
55+
mock_exit.assert_called_once_with(1)
56+
mock_print.assert_any_call(ANY) # Check for any error message
57+
58+
@patch('builtins.print')
59+
def test_main_copy_operations(self, mock_print):
60+
with patch.dict(os.environ, {
61+
'BUILD_PATH': self.build_dir,
62+
'INCLUDED_FILES': f'{self.test_file1}\n{self.test_subdir}\nnon_existent_file.txt',
63+
'PYSTAND_ENTRY_FILE': self.test_file2,
64+
'APPLICATION_NAME': 'my_app'
65+
}):
66+
# 确保在测试前 build_dir 是空的
67+
for item in os.listdir(self.build_dir):
68+
shutil.rmtree(os.path.join(self.build_dir, item)) if os.path.isdir(os.path.join(self.build_dir, item)) else os.remove(os.path.join(self.build_dir, item))
69+
70+
main()
71+
72+
# 检查文件和目录是否被复制
73+
self.assertTrue(os.path.exists(os.path.join(self.build_dir, 'file1.txt')))
74+
self.assertTrue(os.path.isdir(os.path.join(self.build_dir, 'subdir')))
75+
self.assertTrue(os.path.exists(os.path.join(self.build_dir, 'subdir', 'subfile.txt')))
76+
self.assertTrue(os.path.exists(os.path.join(self.build_dir, 'my_app.py')))
77+
78+
# 检查警告信息
79+
print_args_list = [call_args[0][0] for call_args in mock_print.call_args_list]
80+
self.assertTrue(any("Item does not exist, skipped." in args for args in print_args_list))
81+
self.assertTrue(any("non_existent_file.txt" in args for args in print_args_list))
82+
83+
@patch('builtins.print')
84+
def test_main_only_entry_file(self, mock_print):
85+
with patch.dict(os.environ, {
86+
'BUILD_PATH': self.build_dir,
87+
'INCLUDED_FILES': '',
88+
'PYSTAND_ENTRY_FILE': self.test_file1,
89+
'APPLICATION_NAME': 'app_with_no_includes'
90+
}):
91+
# 确保在测试前 build_dir 是空的
92+
for item in os.listdir(self.build_dir):
93+
shutil.rmtree(os.path.join(self.build_dir, item)) if os.path.isdir(os.path.join(self.build_dir, item)) else os.remove(os.path.join(self.build_dir, item))
94+
95+
main()
96+
self.assertTrue(os.path.exists(os.path.join(self.build_dir, 'app_with_no_includes.py')))
97+
self.assertFalse(os.path.exists(os.path.join(self.build_dir, 'file1.txt')))
98+
99+
100+
if __name__ == '__main__':
101+
unittest.main()

0 commit comments

Comments
 (0)