237 lines
9.1 KiB
Python
237 lines
9.1 KiB
Python
# Copyright (C) 2025 The Android Open Source Project
|
|
#
|
|
# 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 sys
|
|
import os
|
|
import glob
|
|
import time
|
|
import subprocess
|
|
import json
|
|
|
|
from golden_manager import GoldenManager, ACCESS_TYPE_SSH, ACCESS_TYPE_TOKEN
|
|
|
|
from utils import execute, ArgParseImpl
|
|
from utils import prompt_helper, PROMPT_YES, PROMPT_NO
|
|
|
|
def line_prompt(prompt, validator=lambda a:True):
|
|
while True:
|
|
res = input(f'{prompt} => ').strip()
|
|
if validator(res):
|
|
return res
|
|
return None
|
|
|
|
CONFIG_NEW_SRC_DIR = 'goldens_dir'
|
|
CONFIG_GOLDENS_BRANCH = 'goldens_branch'
|
|
CONFIG_GOLDENS_UPDATES = 'goldens_updates'
|
|
CONFIG_GOLDENS_DELETES = 'goldens_deletes'
|
|
CONFIG_PUSH_TO_REMOTE = 'push-to-remote'
|
|
CONFIG_COMMIT_MSG = 'commit_msg'
|
|
|
|
def _get_current_branch():
|
|
code, res = execute('git branch --show-current')
|
|
return res.strip()
|
|
|
|
def _file_as_str(fpath):
|
|
with open(fpath, 'r') as f:
|
|
return f.read()
|
|
|
|
def _do_update(golden_manager, config):
|
|
deletes = config[CONFIG_GOLDENS_DELETES]
|
|
updates = config[CONFIG_GOLDENS_UPDATES]
|
|
if len(deletes) == 0 and len(updates) == 0:
|
|
print('Nothing to update. Exiting...')
|
|
exit(0)
|
|
|
|
branch = config[CONFIG_GOLDENS_BRANCH]
|
|
src_dir = config[CONFIG_NEW_SRC_DIR]
|
|
push_to_remote = config[CONFIG_PUSH_TO_REMOTE]
|
|
commit_msg = config[CONFIG_COMMIT_MSG]
|
|
golden_manager.source_from(src_dir, commit_msg, branch,
|
|
updates=updates,
|
|
deletes=deletes,
|
|
push_to_remote=push_to_remote)
|
|
|
|
def _same_image_diffimg(diffimg_path, img1, img2):
|
|
cmd = [diffimg_path, img1, img2]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"diffimg failed with return code {result.returncode}:\n{result.stderr}")
|
|
|
|
output = result.stdout.strip()
|
|
if not output:
|
|
raise RuntimeError("diffimg produced no output")
|
|
|
|
try:
|
|
res_json = json.loads(output)
|
|
return res_json.get('passed', False)
|
|
except json.JSONDecodeError as e:
|
|
raise RuntimeError(f"Failed to parse diffimg output: {e}\nOutput: {output}")
|
|
|
|
def _get_deletes_updates(update_dir, golden_dir, diffimg_path):
|
|
ret_delete = []
|
|
ret_update = []
|
|
|
|
# Scan update_dir for files to potentially update or add
|
|
for ext in ['tif', 'json']:
|
|
# Get relative paths from golden_dir (base) and update_dir (new)
|
|
# Note: glob with root_dir returns relative paths
|
|
base_files = set(glob.glob(f'./**/*.{ext}', root_dir=golden_dir, recursive=True))
|
|
new_files = set(glob.glob(f'./**/*.{ext}', root_dir=update_dir, recursive=True))
|
|
|
|
# Files in base but not in new are candidates for deletion (if we decide to prune)
|
|
# However, update_golden typically only updates/adds based on the new render set.
|
|
# But strict sync might imply deleting missing ones.
|
|
# The original logic was: delete = list(base - new).
|
|
delete_files = list(base_files - new_files)
|
|
|
|
# Files in new but not in base are definitely updates (additions)
|
|
update_files = list(new_files - base_files)
|
|
|
|
# Files in both need comparison
|
|
for fpath in base_files.intersection(new_files):
|
|
base_fpath = os.path.join(golden_dir, fpath)
|
|
new_fpath = os.path.join(update_dir, fpath)
|
|
|
|
is_different = False
|
|
if ext == 'tif':
|
|
is_different = not _same_image_diffimg(diffimg_path, new_fpath, base_fpath)
|
|
elif ext == 'json':
|
|
is_different = _file_as_str(new_fpath) != _file_as_str(base_fpath)
|
|
|
|
if is_different:
|
|
ret_update.append(fpath)
|
|
|
|
ret_update += update_files
|
|
ret_delete += delete_files
|
|
|
|
return ret_delete, ret_update
|
|
|
|
# Ask a bunch of questions to gather the configuration for the update
|
|
def _interactive_mode(base_golden_dir, diffimg_path):
|
|
config = {}
|
|
cur_branch = _get_current_branch()
|
|
if prompt_helper(
|
|
f'Generate the new goldens from your local ' \
|
|
f'Filament branch? (branch={cur_branch})') == PROMPT_YES:
|
|
code, res = execute('bash ./test/renderdiff/generate.sh',
|
|
capture_output=False)
|
|
if code != 0:
|
|
print('Failed to generate new goldens')
|
|
exit(1)
|
|
# Note that this matches RENDER_OUTPUT_DIR in preamble.sh
|
|
config[CONFIG_NEW_SRC_DIR] = os.path.join(os.getcwd(), './out/renderdiff/renders')
|
|
else:
|
|
def validator(src_dir):
|
|
if not os.path.exists(src_dir):
|
|
print(f'Cannot find directory {src_dir}. Please try again.')
|
|
return False
|
|
return True
|
|
|
|
config[CONFIG_NEW_SRC_DIR] = line_prompt(
|
|
'Please provide path of directory containing new goldens',
|
|
validator)
|
|
|
|
if prompt_helper(f'Update new goldens to branch={cur_branch}? '
|
|
'(Note that this refers to a branch in the goldens repo, not the Filament repo.)'
|
|
) == PROMPT_YES:
|
|
config[CONFIG_GOLDENS_BRANCH] = cur_branch
|
|
else:
|
|
config[CONFIG_GOLDENS_BRANCH] = line_prompt('Please provide new branch name for update')
|
|
|
|
if prompt_helper(f'Provide a commit message?') == PROMPT_YES:
|
|
config[CONFIG_COMMIT_MSG] = line_prompt('Message:')
|
|
else:
|
|
config[CONFIG_COMMIT_MSG] = f'Update {time.time()} from filament ({cur_branch})'
|
|
|
|
new_golden_dir = config[CONFIG_NEW_SRC_DIR]
|
|
deletes, updates = _get_deletes_updates(new_golden_dir, base_golden_dir, diffimg_path)
|
|
if len(deletes) + len(updates) != 0:
|
|
prompt = 'The following files will be changed:\n' + \
|
|
'\n'.join([f' {fname} [delete]' for fname in deletes]) + \
|
|
'\n'.join([f' {fname} [update]' for fname in updates]) + \
|
|
'\nIs that ok?'
|
|
if prompt_helper(prompt) == PROMPT_YES:
|
|
config[CONFIG_GOLDENS_DELETES] = deletes
|
|
config[CONFIG_GOLDENS_UPDATES] = updates
|
|
else:
|
|
# We cannot proceed if user answered no.
|
|
exit(1)
|
|
else:
|
|
config[CONFIG_GOLDENS_DELETES] = []
|
|
config[CONFIG_GOLDENS_UPDATES] = []
|
|
|
|
config[CONFIG_PUSH_TO_REMOTE] = \
|
|
prompt_helper(f'Commit golden repo changes to remote?') == PROMPT_YES
|
|
return config
|
|
|
|
if __name__ == "__main__":
|
|
parser = ArgParseImpl()
|
|
parser.add_argument('--branch', help='Branch of the golden repo to write to')
|
|
parser.add_argument('--golden-repo-token', help='Access token for the golden repo')
|
|
parser.add_argument('--push-to-remote', action="store_true", help='Push the golden repo changes to remote')
|
|
parser.add_argument('--diffimg', help='Path to the diffimg tool',
|
|
default='./out/cmake-release/tools/diffimg/diffimg')
|
|
|
|
# write-to-branch mode
|
|
parser.add_argument('--source', help='Directory containing the new goldens')
|
|
parser.add_argument('--commit-msg', help='Message for the commit to the golden repo')
|
|
|
|
# merge-to-main mode (used in postsubmit)
|
|
parser.add_argument('--merge-to-main', action="store_true", help='Merge to main the given branch')
|
|
parser.add_argument('--filament-tag', help='Tag to append to the commit message on merge')
|
|
|
|
args, _ = parser.parse_known_args(sys.argv[1:])
|
|
config = {}
|
|
golden_manager = GoldenManager(
|
|
os.getcwd(),
|
|
access_type=ACCESS_TYPE_SSH if not args.golden_repo_token else ACCESS_TYPE_TOKEN,
|
|
access_token=args.golden_repo_token
|
|
)
|
|
base_golden_dir = golden_manager.directory()
|
|
|
|
diffimg_path = args.diffimg
|
|
# Try to find diffimg if default path doesn't exist, mainly for local interactive use convenience
|
|
if not os.path.exists(diffimg_path):
|
|
# fallback check for debug build
|
|
debug_path = './out/cmake-debug/tools/diffimg/diffimg'
|
|
if os.path.exists(debug_path):
|
|
diffimg_path = debug_path
|
|
|
|
# This is the write-to-branch mode
|
|
if args.branch and args.source and args.commit_msg:
|
|
if not os.path.exists(diffimg_path):
|
|
print(f"Error: diffimg tool not found at {diffimg_path}. Please build it first (e.g., ./build.sh release diffimg)")
|
|
sys.exit(1)
|
|
assert os.path.exists(args.source), f'{args.source} (--source) directory not found'
|
|
deletes, updates = _get_deletes_updates(args.source, base_golden_dir, diffimg_path)
|
|
config = {
|
|
CONFIG_PUSH_TO_REMOTE: args.push_to_remote,
|
|
CONFIG_GOLDENS_BRANCH: args.branch,
|
|
CONFIG_NEW_SRC_DIR: args.source,
|
|
CONFIG_GOLDENS_UPDATES: updates,
|
|
CONFIG_GOLDENS_DELETES: deletes,
|
|
CONFIG_COMMIT_MSG: args.commit_msg,
|
|
}
|
|
_do_update(golden_manager, config)
|
|
# This is the merge-to-main mode
|
|
elif args.branch and args.merge_to_main and args.filament_tag:
|
|
golden_manager.merge_to_main(branch=args.branch, tag=args.filament_tag, push_to_remote=True)
|
|
# Else, we're in interactive mode of write-to-branch (for local execution).
|
|
else:
|
|
if not os.path.exists(diffimg_path):
|
|
print(f"Error: diffimg tool not found at {diffimg_path}. Please build it first (e.g., ./build.sh release diffimg)")
|
|
sys.exit(1)
|
|
config = _interactive_mode(base_golden_dir, diffimg_path)
|
|
_do_update(golden_manager, config)
|