def test_version_note_tuple(self): """Test the version note tuple.""" parser = ChangelogParser(parent_folder="test/fixtures") version = parser._version_note("0.0.0") self.assertIsNone(version) version = parser._version_note("10.1.0-beta1") self.assertEqual("10", version.major) self.assertEqual("1", version.minor) self.assertEqual("0", version.patch) self.assertEqual("", version.url) self.assertEqual("beta1", version.prerelease) self.assertTrue(version.is_prerelease) self.assertEqual("", version.separator) # Not sure what is the separator self.assertEqual("2021/02/08", version.date) self.assertEqual( ("- This is the latest documented version in this changelog\n" "- The changelog module is tested against these lines\n" "- Be careful modifying this file"), version.text, ) version = parser._version_note("10.0.1") self.assertEqual("", version.prerelease) self.assertFalse(version.is_prerelease)
def test_changelog_content_ci_fake(self): """Test specific fake version used in tests.""" parser = ChangelogParser() fake_version_content = parser.content(tag="0.1.2") # expected result expected = ("(This version note is used in unit-tests)\n\n" '- Tag without "v" prefix\n' "- Add a CHANGELOG.md file for testing") self.assertIsInstance(fake_version_content, str) self.assertEqual(expected, fake_version_content)
def test_changelog_latest(self): """Test against the latest special option value. \ See: https://github.com/opengisch/qgis-plugin-ci/pull/33 """ self.assertTrue(ChangelogParser.has_changelog()) parser = ChangelogParser(CHANGELOG_REGEXP) expected_latest = ( "* Tag using a wrong format DD/MM/YYYY according to Keep A Changelog\n" '* Tag without "v" prefix\n' "* Add a CHANGELOG.md file for testing") print(parser.content("latest")) self.assertEqual(expected_latest, parser.content("latest"))
def test_changelog_content_latest(self): """Test against the latest special option value. \ See: https://github.com/opengisch/qgis-plugin-ci/pull/33 """ # expected result expected_latest = ( "- This is the latest documented version in this changelog\n" "- The changelog module is tested against these lines\n" "- Be careful modifying this file") # get latest parser = ChangelogParser(parent_folder="test/fixtures") self.assertEqual(expected_latest, parser.content("latest")) self.assertEqual("10.1.0-beta1", parser.latest_version())
def test_release_changelog(self): """Test if changelog in metadata.txt inside zipped plugin after release command.""" # variables cli_config_changelog = Path( "test/fixtures/.qgis-plugin-ci-test-changelog.yaml") version_to_release = "0.1.2" # load specific parameters with cli_config_changelog.open() as in_cfg: arg_dict = yaml.safe_load(in_cfg) parameters = Parameters(arg_dict) self.assertIsInstance(parameters, Parameters) # get output zip path archive_name = parameters.archive_name( plugin_name=parameters.plugin_path, release_version=version_to_release) # extract last items from changelog parser = ChangelogParser() self.assertTrue(parser.has_changelog()) changelog_lastitems = parser.last_items( count=parameters.changelog_number_of_entries) # Include a changelog release( parameters=parameters, release_version=version_to_release, allow_uncommitted_changes=True, ) # open archive and compare with ZipFile(archive_name, "r") as zip_file: data = zip_file.read(f"{parameters.plugin_path}/metadata.txt") # Changelog self.assertGreater( data.find(bytes(changelog_lastitems, "utf8")), 0, f"changelog detection failed in release: {data}", ) # Commit number self.assertEqual(1, len(re.findall(r"commitNumber=\d+", str(data)))) # Commit sha1 not in the metadata.txt self.assertEqual(0, len(re.findall(r"commitSha1=\d+", str(data))))
def test_changelog_version_note(self): """Test version note named tuple structure and mechanisms.""" # parser parser = ChangelogParser(parent_folder="test/fixtures") self.assertIsInstance(parser.CHANGELOG_FILEPATH, Path) # content parsed changelog_content = parser._parse() self.assertEqual(len(changelog_content), 7) # loop on versions for version in changelog_content: version_note = VersionNote(*version) self.assertIsInstance(version_note.date, str) self.assertTrue(hasattr(version_note, "is_prerelease")) self.assertTrue(hasattr(version_note, "version")) if len(version_note.prerelease): self.assertEqual(version_note.is_prerelease, True)
def test_changelog_content(self): """Test version content from changelog.""" # parser parser = ChangelogParser(parent_folder="test/fixtures") self.assertIsInstance(parser.CHANGELOG_FILEPATH, Path) # Unreleased doesn't count self.assertEqual(7, len(parser._parse())) # This version doesn't exist self.assertIsNone(parser.content("0.0.0")) expected_checks = { "10.1.0-beta1": ("- This is the latest documented version in this changelog\n" "- The changelog module is tested against these lines\n" "- Be careful modifying this file"), "10.1.0-alpha1": ( "- This is a version with a prerelease in this changelog\n" "- The changelog module is tested against these lines\n" "- Be careful modifying this file" # "\n" TODO Fixed section is missing # "- trying with a subsection in a version note" ), "10.0.1": "- End of year version", "10.0.0": "- A\n- B\n- C", "9.10.1": "- D\n- E\n- F", "v0.1.1": ('* Tag with a "v" prefix to check the regular expression\n' "* Previous version"), "0.1.0": "* Very old version", } for version, expected in expected_checks.items(): with self.subTest(i=version): self.assertEqual(parser.content(version), expected)
def test_different_changelog_file(self): """Test against a different changelog filename.""" old = Path("test/fixtures/CHANGELOG.md") new_folder = Path(tempfile.mkdtemp()) new_path = new_folder / Path("CHANGELOG-branch-X.md") self.assertFalse(new_path.exists()) new_path.write_text(old.read_text()) self.assertTrue( ChangelogParser.has_changelog( parent_folder=new_folder, changelog_path=new_path, ))
def test_changelog_last_items(self): """Test last items from changelog.""" # on fixture changelog parser = ChangelogParser(parent_folder="test/fixtures") last_items = parser.last_items(3) self.assertIsInstance(last_items, str) # on repository changelog parser = ChangelogParser() last_items = parser.last_items(3) self.assertIsInstance(last_items, str)
def test_changelog_parser(self): """ Test we can parse a changelog with a regex. """ self.assertTrue(ChangelogParser.has_changelog()) parser = ChangelogParser(CHANGELOG_REGEXP) self.assertIsNone(parser.content("0.0.0"), "") expected = ( "* Tag using a wrong format DD/MM/YYYY according to Keep A Changelog\n" '* Tag without "v" prefix\n' "* Add a CHANGELOG.md file for testing") self.assertEqual(parser.content("0.1.2"), expected) expected = ( "\n " "Version 0.1.2 :\n " "* Tag using a wrong format DD/MM/YYYY according to Keep A Changelog\n " '* Tag without "v" prefix\n ' "* Add a CHANGELOG.md file for testing\n" "\n") self.assertEqual(parser.last_items(1), expected) expected = """ Version 0.1.2 : * Tag using a wrong format DD/MM/YYYY according to Keep A Changelog * Tag without "v" prefix * Add a CHANGELOG.md file for testing Version v0.1.1 : * Tag using a correct format YYYY-MM-DD according to Keep A Changelog * Tag with a "v" prefix to check the regular expression * Previous version Version 0.1.0 : * Very old version """ self.assertEqual(parser.last_items(3), expected)
def test_has_changelog(self): """Test changelog path logic.""" # using this repository as parent folder self.assertTrue(ChangelogParser.has_changelog()) self.assertIsInstance(ChangelogParser.CHANGELOG_FILEPATH, Path) # using the fixture subfolder as string self.assertTrue( ChangelogParser.has_changelog(parent_folder="test/fixtures")) self.assertIsInstance(ChangelogParser.CHANGELOG_FILEPATH, Path) # using the fixture subfolder as pathlib.Path self.assertTrue( ChangelogParser.has_changelog(parent_folder=Path("test/fixtures"))) self.assertIsInstance(ChangelogParser.CHANGELOG_FILEPATH, Path) # with a path to a file, must raise a type error with self.assertRaises(TypeError): ChangelogParser.has_changelog(parent_folder=Path(__file__)) self.assertIsNone(ChangelogParser.CHANGELOG_FILEPATH, None) # with a path to a folder which doesn't exist, must raise a file exists error with self.assertRaises(FileExistsError): ChangelogParser.has_changelog(parent_folder=Path("imaginary_path"))
def main(): # create the top-level parser parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("-v", "--version", help="print the version and exit", action="store_true") subparsers = parser.add_subparsers(title="commands", description="qgis-plugin-ci command", dest="command") # package package_parser = subparsers.add_parser( "package", help="creates an archive of the plugin") package_parser.add_argument("release_version", help="The version to be released") package_parser.add_argument( "--transifex-token", help= "The Transifex API token. If specified translations will be pulled and compiled.", ) package_parser.add_argument( "-u", "--plugin-repo-url", help= "If specified, a XML repository file will be created in the current directory, the zip URL will use this parameter.", ) package_parser.add_argument( "-c", "--allow-uncommitted-changes", action="store_true", help= "If omitted, uncommitted changes are not allowed before packaging. If specified and some changes are " "detected, a hard reset on a stash create will be used to revert changes made by qgis-plugin-ci.", ) package_parser.add_argument( "-d", "--disable-submodule-update", action="store_true", help= "If omitted, a git submodule is updated. If specified, git submodules will not be updated/initialized before packaging.", ) # changelog changelog_parser = subparsers.add_parser("changelog", help="gets the changelog content") changelog_parser.add_argument( "release_version", help="The version to be released. If nothing is speficied, \ the latest version specified into the changelog is used.", default="latest", ) # release release_parser = subparsers.add_parser("release", help="release the plugin") release_parser.add_argument("release_version", help="The version to be released (x.y.z).") release_parser.add_argument( "--release-tag", help="The release tag, if different from the version (e.g. vx.y.z).", ) release_parser.add_argument( "--transifex-token", help= "The Transifex API token. If specified translations will be pulled and compiled.", ) release_parser.add_argument( "--github-token", help= "The Github API token. If specified, the archive will be pushed to an already existing release.", ) release_parser.add_argument( "--create-plugin-repo", action="store_true", help= "Will create a XML repo as a Github release asset. Github token is required.", ) release_parser.add_argument( "--allow-uncommitted-changes", action="store_true", help= "If omitted, uncommitted changes are not allowed before releasing. If specified and some changes are " "detected, a hard reset on a stash create will be used to revert changes made by qgis-plugin-ci.", ) release_parser.add_argument( "--disable-submodule-update", action="store_true", help= "If omitted, a git submodule is updated. If specified, git submodules will not be updated/initialized before packaging.", ) release_parser.add_argument( "--osgeo-username", help="The Osgeo user name to publish the plugin.") release_parser.add_argument( "--osgeo-password", help="The Osgeo password to publish the plugin.") # pull-translation pull_tr_parser = subparsers.add_parser( "pull-translation", help="pull translations from Transifex") pull_tr_parser.add_argument("transifex_token", help="The Transifex API token") pull_tr_parser.add_argument("--compile", action="store_true", help="Will compile TS files into QM files") # push-translation push_tr_parser = subparsers.add_parser( "push-translation", help="update strings and push translations") push_tr_parser.add_argument("transifex_token", help="The Transifex API token") args = parser.parse_args() # print the version and exit if args.version: import pkg_resources print("qgis-plugin-ci version: {}".format( pkg_resources.get_distribution("qgis-plugin-ci").version)) parser.exit() # if no command is passed, print the help and exit if not args.command: parser.print_help() parser.exit() exit_val = 0 if os.path.isfile(".qgis-plugin-ci"): # We read the .qgis-plugin-ci file with open(".qgis-plugin-ci", "r", encoding="utf8") as f: arg_dict = yaml.safe_load(f) else: config = configparser.ConfigParser() config.read("setup.cfg") if "qgis-plugin-ci" in config.sections(): # We read the setup.cfg file arg_dict = dict(config.items("qgis-plugin-ci")) else: # We don't have either a .qgis-plugin-ci or a setup.cfg if args.command == "changelog": # but for the "changelog" sub command, the config file is not required, we can continue arg_dict = dict() else: raise ConfigurationNotFound( ".qgis-plugin-ci or setup.cfg with a 'qgis-plugin-ci' section have not been found." ) parameters = Parameters(arg_dict) # CHANGELOG if args.command == "changelog": try: c = ChangelogParser(changelog_path=parameters.changelog_path, ) content = c.content(args.release_version) if content: print(content) except Exception: # Better to be safe pass return exit_val # PACKAGE if args.command == "package": release( parameters, release_version=args.release_version, transifex_token=args.transifex_token, allow_uncommitted_changes=args.allow_uncommitted_changes, plugin_repo_url=args.plugin_repo_url, disable_submodule_update=args.disable_submodule_update, ) # RELEASE elif args.command == "release": release( parameters, release_version=args.release_version, release_tag=args.release_tag, transifex_token=args.transifex_token, github_token=args.github_token, upload_plugin_repo_github=args.create_plugin_repo, osgeo_username=args.osgeo_username, osgeo_password=args.osgeo_password, allow_uncommitted_changes=args.allow_uncommitted_changes, disable_submodule_update=args.disable_submodule_update, ) # TRANSLATION PULL elif args.command == "pull-translation": t = Translation(parameters, args.transifex_token) t.pull() if args.compile: t.compile_strings() # TRANSLATION PUSH elif args.command == "push-translation": t = Translation(parameters, args.transifex_token) t.update_strings() t.push() return exit_val
def create_archive( parameters: Parameters, release_version: str, archive_name: str, add_translations: bool = False, allow_uncommitted_changes: bool = False, is_prerelease: bool = False, raise_min_version: str = None, disable_submodule_update: bool = False, ): repo = git.Repo() top_tar_handle, top_tar_file = mkstemp(suffix=".tar") # keep track of current state initial_stash = None diff = repo.index.diff(None) if diff: print("Uncommitted changes:") for diff in diff: print(diff) if not allow_uncommitted_changes: raise UncommitedChanges( "You have uncommitted changes. Stash or commit them or use --allow-uncommitted-changes." ) else: initial_stash = repo.git.stash("create") # changelog if parameters.changelog_include: parser = ChangelogParser(parameters.changelog_regexp) if parser.has_changelog(): try: content = parser.last_items( parameters.changelog_number_of_entries) if content: replace_in_file( "{}/metadata.txt".format(parameters.plugin_path), r"^changelog=.*$", "changelog={}".format(content), ) except Exception as e: # Do not fail the release process if something is wrong when parsing the changelog replace_in_file( "{}/metadata.txt".format(parameters.plugin_path), r"^changelog=.*$", "", ) print( "An exception occurred while parsing the changelog file : {}" .format(e)) else: # Remove the changelog line replace_in_file("{}/metadata.txt".format(parameters.plugin_path), r"^changelog=.*$", "") # set version in metadata replace_in_file( "{}/metadata.txt".format(parameters.plugin_path), r"^version=.*$", "version={}".format(release_version), ) # set the plugin as experimental on a pre-release if is_prerelease: replace_in_file( "{}/metadata.txt".format(parameters.plugin_path), r"^experimental=.*$", "experimental={}".format(True if is_prerelease else False), ) if raise_min_version: replace_in_file( "{}/metadata.txt".format(parameters.plugin_path), r"^qgisMinimumVersion=.*$", "qgisMinimumVersion={}".format(raise_min_version), ) # replace any DEBUG=False in all Python files if not is_prerelease: for file in glob("{}/**/*.py".format(parameters.plugin_path), recursive=True): replace_in_file(file, r"^DEBUG\s*=\s*True", "DEBUG = False") # keep track of current state try: stash = repo.git.stash("create") except git.exc.GitCommandError: stash = "HEAD" if stash == "" or stash is None: stash = "HEAD" # create TAR archive print("archive plugin with stash: {}".format(stash)) repo.git.archive(stash, "-o", top_tar_file, parameters.plugin_path) # adding submodules for submodule in repo.submodules: _, sub_tar_file = mkstemp(suffix=".tar") if submodule.path.split("/")[0] != parameters.plugin_path: print("skipping submodule not in plugin source directory ({})". format(submodule.name)) continue if not disable_submodule_update: submodule.update(init=True) sub_repo = submodule.module() print("archive submodule:", sub_repo) sub_repo.git.archive("HEAD", "--prefix", "{}/".format(submodule.path), "-o", sub_tar_file) with tarfile.open(top_tar_file, mode="a") as tt: with tarfile.open(sub_tar_file, mode="r:") as st: for m in st.getmembers(): # print('adding', m, m.type, m.isfile()) if not m.isfile(): continue tt.add(m.name) # add translation files if add_translations: with tarfile.open(top_tar_file, mode="a") as tt: print("adding translations") for file in glob("{}/i18n/*.qm".format(parameters.plugin_path)): print(" adding translation: {}".format( os.path.basename(file))) # https://stackoverflow.com/a/48462950/1548052 tt.add(file) # compile qrc files pyqt5ac.main(ioPaths=[[ "{}/*.qrc".format(parameters.plugin_path), "{}/%%FILENAME%%_rc.py".format(parameters.plugin_path), ]]) for file in glob("{}/*_rc.py".format(parameters.plugin_path)): with tarfile.open(top_tar_file, mode="a") as tt: print(" adding resource: {}".format(file)) # https://stackoverflow.com/a/48462950/1548052 tt.add(file) # converting to ZIP # why using TAR before? because it provides the prefix and makes things easier with zipfile.ZipFile(file=archive_name, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: # adding the content of TAR archive with tarfile.open(top_tar_file, mode="r:") as tt: for m in tt.getmembers(): if m.isdir(): continue f = tt.extractfile(m) fl = f.read() fn = m.name zf.writestr(fn, fl) print("-------") print("files in ZIP archive ({}):".format(archive_name)) with zipfile.ZipFile(file=archive_name, mode="r") as zf: for f in zf.namelist(): print(f) print("-------") # checkout to reset changes if initial_stash: repo.git.reset("--hard", initial_stash) repo.git.reset("HEAD^") else: repo.git.checkout("--", ".")
def main(): # create the top-level parser parser = argparse.ArgumentParser() parser.add_argument("-v", "--version", help="print the version and exit", action='store_true') subparsers = parser.add_subparsers(title='commands', description='qgis-plugin-ci command', dest='command') # package package_parser = subparsers.add_parser( 'package', help='creates an archive of the plugin') package_parser.add_argument('release_version', help='The version to be released') package_parser.add_argument( '--transifex-token', help= 'The Transifex API token. If specified translations will be pulled and compiled.' ) package_parser.add_argument( '--plugin-repo-url', help= 'If specified, a XML repository file will be created in the current directory, the zip URL will use this parameter.' ) package_parser.add_argument( '--allow-uncommitted-changes', action='store_true', help= 'If omitted, uncommitted changes are not allowed before packaging. If specified and some changes are ' 'detected, a hard reset on a stash create will be used to revert changes made by qgis-plugin-ci.' ) package_parser.add_argument( '--disable-submodule-update', action='store_true', help= 'If omitted, a git submodule is updated. If specified, git submodules will not be updated/initialized before packaging.' ) # changelog changelog_parser = subparsers.add_parser('changelog', help='gets the changelog content') changelog_parser.add_argument('release_version', help='The version to be released') # release release_parser = subparsers.add_parser('release', help='release the plugin') release_parser.add_argument('release_version', help='The version to be released') release_parser.add_argument( '--transifex-token', help= 'The Transifex API token. If specified translations will be pulled and compiled.' ) release_parser.add_argument( '--github-token', help= 'The Github API token. If specified, the archive will be pushed to an already existing release.' ) release_parser.add_argument( '--create-plugin-repo', action='store_true', help= 'Will create a XML repo as a Github release asset. Github token is required.' ) release_parser.add_argument( '--allow-uncommitted-changes', action='store_true', help= 'If omitted, uncommitted changes are not allowed before releasing. If specified and some changes are ' 'detected, a hard reset on a stash create will be used to revert changes made by qgis-plugin-ci.' ) release_parser.add_argument( '--disable-submodule-update', action='store_true', help= 'If omitted, a git submodule is updated. If specified, git submodules will not be updated/initialized before packaging.' ) release_parser.add_argument( '--osgeo-username', help='The Osgeo user name to publish the plugin.') release_parser.add_argument( '--osgeo-password', help='The Osgeo password to publish the plugin.') # pull-translation pull_tr_parser = subparsers.add_parser( 'pull-translation', help='pull translations from Transifex') pull_tr_parser.add_argument('transifex_token', help='The Transifex API token') pull_tr_parser.add_argument('--compile', action='store_true', help='Will compile TS files into QM files') # push-translation push_tr_parser = subparsers.add_parser( 'push-translation', help='update strings and push translations') push_tr_parser.add_argument('transifex_token', help='The Transifex API token') args = parser.parse_args() # print the version and exit if args.version: import pkg_resources print('qgis-plugin-ci version: {}'.format( pkg_resources.get_distribution('qgis-plugin-ci').version)) parser.exit() # if no command is passed, print the help and exit if not args.command: parser.print_help() parser.exit() exit_val = 0 if os.path.isfile(".qgis-plugin-ci"): arg_dict = yaml.safe_load(open(".qgis-plugin-ci")) else: config = configparser.ConfigParser() config.read("setup.cfg") if "qgis-plugin-ci" not in config.sections(): raise ConfigurationNotFound( ".qgis-plugin-ci or setup.cfg with a 'qgis-plugin-ci' section have not been found." ) arg_dict = dict(config.items("qgis-plugin-ci")) parameters = Parameters(arg_dict) # PACKAGE if args.command == 'package': release( parameters, release_version=args.release_version, transifex_token=args.transifex_token, allow_uncommitted_changes=args.allow_uncommitted_changes, plugin_repo_url=args.plugin_repo_url, disable_submodule_update=args.disable_submodule_update, ) # RELEASE elif args.command == 'release': release( parameters, release_version=args.release_version, transifex_token=args.transifex_token, github_token=args.github_token, upload_plugin_repo_github=args.create_plugin_repo, osgeo_username=args.osgeo_username, osgeo_password=args.osgeo_password, allow_uncommitted_changes=args.allow_uncommitted_changes, disable_submodule_update=args.disable_submodule_update, ) # CHANGELOG elif args.command == 'changelog': try: c = ChangelogParser(parameters.changelog_regexp) content = c.content(args.release_version) if content: print(content) except Exception: # Better to be safe pass # TRANSLATION PULL elif args.command == 'pull-translation': t = Translation(parameters, args.transifex_token) t.pull() if args.compile: t.compile_strings() # TRANSLATION PUSH elif args.command == 'push-translation': t = Translation(parameters, args.transifex_token) t.update_strings() t.push() return exit_val