diff --git a/conans/build_info/build_info.py b/conans/build_info/build_info.py new file mode 100644 index 00000000000..07893657e41 --- /dev/null +++ b/conans/build_info/build_info.py @@ -0,0 +1,324 @@ +import datetime +import json +import os +from collections import defaultdict, namedtuple +from six.moves.urllib.parse import urlparse, urljoin + +import requests + +from conans.client.cache.cache import ClientCache +from conans.client.rest import response_to_str +from conans.errors import AuthenticationException, RequestErrorException, ConanException +from conans.model.ref import ConanFileReference +from conans.paths import get_conan_user_home +from conans.util.files import save + + +class Artifact(namedtuple('Artifact', ["sha1", "md5", "name", "id"])): + def __hash__(self): + return hash(self.sha1) + + +def _parse_options(contents): + for line in contents.splitlines(): + key, value = line.split("=") + yield "options.{}".format(key), value + + +def _parse_profile(contents): + import configparser + + config = configparser.ConfigParser() + config.read_string(contents) + + for section, values in config._sections.items(): + for key, value in values.items(): + yield "{}.{}".format(section, key), value + + +class BuildInfoCreator(object): + def __init__(self, output, build_info_file, lockfile, multi_module=True, skip_env=True, + user=None, password=None, apikey=None): + self._build_info_file = build_info_file + self._lockfile = lockfile + self._multi_module = multi_module + self._skip_env = skip_env + self._user = user + self._password = password + self._apikey = apikey + self._conan_cache = ClientCache(os.path.join(get_conan_user_home(), ".conan"), output) + + def parse_pref(self, pref): + ref = ConanFileReference.loads(pref, validate=False) + rrev = ref.revision.split("#")[0].split(":")[0] + pid = ref.revision.split("#")[0].split(":")[1] + prev = ref.revision.split("#")[1] + return { + "name": ref.name, + "version": ref.version, + "user": ref.user, + "channel": ref.channel, + "rrev": rrev, + "pid": pid, + "prev": prev + } + + def _get_reference(self, pref): + r = self.parse_pref(pref) + if r.get("user") and r.get("channel"): + return "{name}/{version}@{user}/{channel}".format(**r) + else: + return "{name}/{version}".format(**r) + + def _get_package_reference(self, pref): + r = self.parse_pref(pref) + return "{reference}:{pid}".format(reference=self._get_reference(pref), **r) + + def _get_metadata_artifacts(self, metadata, request_path, use_id=False, name_format="{}", + package_id=None): + ret = {} + need_sources = False + if package_id: + data = metadata.packages[package_id].checksums + else: + data = metadata.recipe.checksums + need_sources = not ("conan_sources.tgz" in data) + + for name, value in data.items(): + name_or_id = name_format.format(name) + ret[value["sha1"]] = {"md5": value["md5"], + "name": name_or_id if not use_id else None, + "id": name_or_id if use_id else None} + if need_sources: + remote_name = metadata.recipe.remote + remotes = self._conan_cache.registry.load_remotes() + remote_url = remotes[remote_name].url + parsed_uri = urlparse(remote_url) + base_url = "{uri.scheme}://{uri.netloc}/artifactory/api/storage/conan/".format( + uri=parsed_uri) + request_url = urljoin(base_url, "{}/conan_sources.tgz".format(request_path)) + if self._user and self._password: + response = requests.get(request_url, auth=(self._user, self._password)) + elif self._apikey: + response = requests.get(request_url, headers={"X-JFrog-Art-Api": self._apikey}) + else: + response = requests.get(request_url) + + if response.status_code == 200: + data = response.json() + ret[data["checksums"]["sha1"]] = {"md5": data["checksums"], + "name": "conan_sources.tgz", + "id": None} + elif response.status_code == 401: + raise AuthenticationException(response_to_str(response)) + else: + raise RequestErrorException(response_to_str(response)) + + return set([Artifact(k, **v) for k, v in ret.items()]) + + def _get_recipe_artifacts(self, pref, add_prefix, use_id): + r = self.parse_pref(pref) + if r.get("user") and r.get("channel"): + ref = "{name}/{version}@{user}/{channel}#{rrev}".format(**r) + else: + ref = "{name}/{version}#{rrev}".format(**r) + reference = ConanFileReference.loads(ref) + package_layout = self._conan_cache.package_layout(reference) + metadata = package_layout.load_metadata() + name_format = "{} :: {{}}".format(self._get_reference(pref)) if add_prefix else "{}" + if r.get("user") and r.get("channel"): + url = "{user}/{name}/{version}/{channel}/{rrev}/export".format(**r) + else: + url = "_/{name}/{version}/_/{rrev}/export".format(**r) + + return self._get_metadata_artifacts(metadata, url, name_format=name_format, use_id=use_id) + + def _get_package_artifacts(self, pref, add_prefix, use_id): + r = self.parse_pref(pref) + if r.get("user") and r.get("channel"): + ref = "{name}/{version}@{user}/{channel}#{rrev}".format(**r) + else: + ref = "{name}/{version}#{rrev}".format(**r) + reference = ConanFileReference.loads(ref) + package_layout = self._conan_cache.package_layout(reference) + metadata = package_layout.load_metadata() + name_format = "{} :: {{}}".format(self._get_package_reference(pref)) if add_prefix else "{}" + if r.get("user") and r.get("channel"): + url = "{user}/{name}/{version}/{channel}/{rrev}/package/{pid}/{prev}".format(**r) + else: + url = "_/{name}/{version}/_/{rrev}/package/{pid}/{prev}".format(**r) + arts = self._get_metadata_artifacts(metadata, url, name_format=name_format, use_id=use_id, + package_id=r["pid"]) + return arts + + def process_lockfile(self): + modules = defaultdict(lambda: {"id": None, "artifacts": set(), "dependencies": set()}) + + def _gather_deps(node_uid, contents, func): + node_content = contents["graph_lock"]["nodes"].get(node_uid) + artifacts = func(node_content["pref"], add_prefix=True, use_id=True) + for _, id_node in node_content.get("requires", {}).items(): + artifacts.update(_gather_deps(id_node, contents, func)) + return artifacts + + with open(self._lockfile) as json_data: + data = json.load(json_data) + + # Gather modules, their artifacts and recursively all required artifacts + for _, node in data["graph_lock"]["nodes"].items(): + pref = node["pref"] + if node.get("modified"): # Work only on generated nodes + # Create module for the recipe reference + recipe_key = self._get_reference(pref) + modules[recipe_key]["id"] = recipe_key + modules[recipe_key]["artifacts"].update( + self._get_recipe_artifacts(pref, add_prefix=not self._multi_module, + use_id=False)) + # TODO: what about `python_requires`? + # TODO: can we associate any properties to the recipe? Profile/options may be different per lockfile + + # Create module for the package_id + package_key = self._get_package_reference(pref) if self._multi_module else recipe_key + modules[package_key]["id"] = package_key + modules[package_key]["artifacts"].update( + self._get_package_artifacts(pref, add_prefix=not self._multi_module, + use_id=False)) + + # Recurse requires + if node.get("requires"): + for _, node_id in node["requires"].items(): + modules[recipe_key]["dependencies"].update( + _gather_deps(node_id, data, self._get_recipe_artifacts)) + modules[package_key]["dependencies"].update( + _gather_deps(node_id, data, self._get_package_artifacts)) + + # TODO: Is the recipe a 'dependency' of the package + + return modules + + def create(self): + properties = self._conan_cache.read_put_headers() + modules = self.process_lockfile() + # Add extra information + ret = {"version": "1.0.1", + "name": properties["artifact_property_build.name"], + "number": properties["artifact_property_build.number"], + "type": "GENERIC", + "started": datetime.datetime.utcnow().isoformat().split(".")[0] + ".000Z", + "buildAgent": {"name": "Conan Client", "version": "1.X"}, + "modules": list(modules.values())} + + if not self._skip_env: + excluded = ["secret", "key", "password"] + environment = {"buildInfo.env.{}".format(k): v for k, v in os.environ.items() if + k not in excluded} + ret["properties"] = environment + + def dump_custom_types(obj): + if isinstance(obj, set): + artifacts = [{k: v for k, v in o._asdict().items() if v is not None} for o in obj] + return sorted(artifacts, key=lambda u: u.get("name") or u.get("id")) + raise TypeError + + with open(self._build_info_file, "w") as f: + f.write(json.dumps(ret, indent=4, default=dump_custom_types)) + + +def create_build_info(output, build_info_file, lockfile, multi_module, skip_env, user, password, + apikey): + bi = BuildInfoCreator(output, build_info_file, lockfile, multi_module, skip_env, user, password, + apikey) + bi.create() + + +def start_build_info(output, build_name, build_number): + paths = ClientCache(os.path.join(get_conan_user_home(), ".conan"), output) + content = "artifact_property_build.name={}\n" \ + "artifact_property_build.number={}\n".format(build_name, build_number) + artifact_properties_file = paths.put_headers_path + try: + save(artifact_properties_file, content) + except Exception: + raise ConanException("Can't write properties file in %s" % artifact_properties_file) + + +def stop_build_info(output): + paths = ClientCache(os.path.join(get_conan_user_home(), ".conan"), output) + artifact_properties_file = paths.put_headers_path + try: + save(artifact_properties_file, "") + except Exception: + raise ConanException("Can't write properties file in %s" % artifact_properties_file) + + +def publish_build_info(build_info_file, url, user, password, apikey): + with open(build_info_file) as json_data: + parsed_uri = urlparse(url) + request_url = "{uri.scheme}://{uri.netloc}/artifactory/api/build".format(uri=parsed_uri) + if user and password: + response = requests.put(request_url, headers={"Content-Type": "application/json"}, + data=json_data, auth=(user, password)) + elif apikey: + response = requests.put(request_url, headers={"Content-Type": "application/json", + "X-JFrog-Art-Api": apikey}, + data=json_data) + else: + response = requests.put(request_url) + + if response.status_code == 401: + raise AuthenticationException(response_to_str(response)) + elif response.status_code != 204: + raise RequestErrorException(response_to_str(response)) + + +def find_module(build_info, module_id): + for it in build_info["modules"]: + if it["id"] == module_id: + return it + new_module = {"id": module_id, "artifacts": [], "dependencies": []} + build_info["modules"].append(new_module) + return new_module + + +def merge_artifacts(lhs, rhs, key, cmp_key): + ret = {it[cmp_key]: it for it in lhs[key]} + for art in rhs[key]: + art_cmp_key = art[cmp_key] + if art_cmp_key in ret: + assert art[cmp_key] == ret[art_cmp_key][cmp_key], \ + "({}) {} != {} for sha1={}".format(cmp_key, art[cmp_key], ret[art_cmp_key][cmp_key], + art_cmp_key) + else: + ret[art_cmp_key] = art + + return [value for _, value in ret.items()] + + +def merge_buildinfo(lhs, rhs): + if not lhs or not rhs: + return lhs or rhs + + # Check they are compatible + assert lhs["version"] == rhs["version"] + assert lhs["name"] == rhs["name"] + assert lhs["number"] == rhs["number"] + + for rhs_module in rhs["modules"]: + lhs_module = find_module(lhs, rhs_module["id"]) + lhs_module["artifacts"] = merge_artifacts(lhs_module, rhs_module, key="artifacts", + cmp_key="name") + lhs_module["dependencies"] = merge_artifacts(lhs_module, rhs_module, key="dependencies", + cmp_key="id") + return lhs + + +def update_build_info(buildinfo, output_file): + build_info = {} + for it in buildinfo: + with open(it) as json_data: + data = json.load(json_data) + + build_info = merge_buildinfo(build_info, data) + + with open(output_file, "w") as f: + f.write(json.dumps(build_info, indent=4)) diff --git a/conans/build_info/command.py b/conans/build_info/command.py index 16ac99b84aa..f0eb0762c8a 100644 --- a/conans/build_info/command.py +++ b/conans/build_info/command.py @@ -1,13 +1,26 @@ import argparse import json import os +import sys from conans.build_info.conan_build_info import get_build_info +from conans.build_info.build_info import start_build_info, stop_build_info, create_build_info, \ + update_build_info, publish_build_info +from conans.errors import ConanException from conans.util.files import save +from conans.client.output import ConanOutput def run(): + if len(sys.argv) > 2 and sys.argv[1] == "--v2": + sys.argv.pop(1) + runv2() + else: + runv1() + +def runv1(): + output = ConanOutput(sys.stdout, sys.stderr, True) parser = argparse.ArgumentParser(description='Extracts build-info from a specified ' 'conan trace log and return a valid JSON') parser.add_argument('trace_path', help='Path to the conan trace log file e.g.: ' @@ -16,25 +29,107 @@ def run(): help='Optional file to output the JSON contents, if not specified the JSON' ' will be printed to stdout') - args = parser.parse_args() + try: + args = parser.parse_args() - if not os.path.exists(args.trace_path): - print("Error, conan trace log not found! '%s'" % args.trace_path) - exit(1) - if args.output and not os.path.exists(os.path.dirname(args.output)): - print("Error, output file directory not found! '%s'" % args.trace_path) - exit(1) + if not os.path.exists(args.trace_path): + output.error("Conan trace log not found! '%s'" % args.trace_path) + exit(1) + if args.output and not os.path.exists(os.path.dirname(args.output)): + output.error("Output file directory not found! '%s'" % args.trace_path) + exit(1) - try: info = get_build_info(args.trace_path) the_json = json.dumps(info.serialize()) if args.output: save(args.output, the_json) else: - print(the_json) + output.write(the_json) except Exception as exc: - print(exc) + output.error(exc) exit(1) + except SystemExit: + output.writeln("") + output.warn("Use 'conan_build_info --v2' to see the usage of the new recommended way to " + "generate build info using lockfiles") + + +def runv2(): + output = ConanOutput(sys.stdout, sys.stderr, True) + parser = argparse.ArgumentParser( + description="Generates build info build info from lockfiles information", + prog="conan_build_info") + subparsers = parser.add_subparsers(dest="subcommand", help="sub-command help") + + parser_start = subparsers.add_parser("start", + help="Command to incorporate to the " + "artifacts.properties the build name and number") + parser_start.add_argument("build_name", type=str, help="build name to assign") + parser_start.add_argument("build_number", type=int, help="build number to assign") + + parser_stop = subparsers.add_parser("stop", + help="Command to remove from the artifacts.properties " + "the build name and number") + + parser_create = subparsers.add_parser("create", + help="Command to generate a build info json from a " + "lockfile") + parser_create.add_argument("build_info_file", type=str, + help="build info json for output") + parser_create.add_argument("--lockfile", type=str, required=True, help="input lockfile") + parser_create.add_argument("--multi-module", nargs="?", default=True, + help="if enabled, the module_id will be identified by the " + "recipe reference plus the package ID") + parser_create.add_argument("--skip-env", nargs="?", default=True, + help="capture or not the environment") + parser_create.add_argument("--user", type=str, nargs="?", default=None, help="user") + parser_create.add_argument("--password", type=str, nargs="?", default=None, help="password") + parser_create.add_argument("--apikey", type=str, nargs="?", default=None, help="apikey") + + parser_update = subparsers.add_parser("update", + help="Command to update a build info json with another one") + parser_update.add_argument("buildinfo", nargs="+", help="buildinfo files to merge") + parser_update.add_argument("--output-file", default="buildinfo.json", + help="path to generated build info file") + + parser_publish = subparsers.add_parser("publish", + help="Command to publish the build info to Artifactory") + parser_publish.add_argument("buildinfo", type=str, + help="build info to upload") + parser_publish.add_argument("--url", type=str, required=True, help="url") + parser_publish.add_argument("--user", type=str, nargs="?", default=None, help="user") + parser_publish.add_argument("--password", type=str, nargs="?", default=None, help="password") + parser_publish.add_argument("--apikey", type=str, nargs="?", default=None, help="apikey") + + def check_credential_arguments(): + if args.user and args.apikey: + parser.error("Please select one authentificacion method --user USER " + "--password PASSWORD or --apikey APIKEY") + if args.user and not args.password: + parser.error( + "Please specify a password for user '{}' with --password PASSWORD".format( + args.user)) + + try: + args = parser.parse_args() + if args.subcommand == "start": + start_build_info(output, args.build_name, args.build_number) + if args.subcommand == "stop": + stop_build_info(output) + if args.subcommand == "create": + check_credential_arguments() + create_build_info(output, args.build_info_file, args.lockfile, args.multi_module, + args.skip_env, args.user, args.password, args.apikey) + if args.subcommand == "update": + update_build_info(args.buildinfo, args.output_file) + if args.subcommand == "publish": + check_credential_arguments() + publish_build_info(args.buildinfo, args.url, args.user, args.password, + args.apikey) + except ConanException as exc: + output.error(exc) + except Exception as exc: + output.error(exc) if __name__ == "__main__": diff --git a/conans/test/integration/test_build_info_creation.py b/conans/test/integration/test_build_info_creation.py new file mode 100644 index 00000000000..865e47ec769 --- /dev/null +++ b/conans/test/integration/test_build_info_creation.py @@ -0,0 +1,173 @@ +import json +import os +import shutil +import sys +import textwrap +import unittest + +from mock import patch, Mock + +from conans.client.cache.cache import ClientCache +from conans.model.graph_lock import LOCKFILE +from conans.build_info.command import run +from conans.test.utils.test_files import temp_folder +from conans.test.utils.tools import TestClient, TestBufferConanOutput, TestServer + + +class MyBuildInfoCreation(unittest.TestCase): + @patch("conans.build_info.build_info.ClientCache") + def test_build_info_start(self, mock_cache): + conan_user_home = temp_folder(True) + mock_cache.return_value = ClientCache(os.path.join(conan_user_home, ".conan"), + TestBufferConanOutput()) + sys.argv = ["conan_build_info", "--v2", "start", "MyBuildName", "42"] + run() + with open(mock_cache.return_value.put_headers_path) as f: + content = f.read() + self.assertIn("MyBuildName", content) + self.assertIn("42", content) + + @patch("conans.build_info.build_info.ClientCache") + def test_build_info_stop(self, mock_cache): + conan_user_home = temp_folder(True) + mock_cache.return_value = ClientCache(os.path.join(conan_user_home, ".conan"), + TestBufferConanOutput()) + sys.argv = ["conan_build_info", "--v2", "stop"] + run() + with open(mock_cache.return_value.put_headers_path) as f: + content = f.read() + self.assertEqual("", content) + + def mock_response(url, data=None, **kwargs): + mock_resp = Mock() + mock_resp.status_code = 204 + if "api/build" in url: + mock_resp.status_code = 204 + if kwargs.get("auth", None) and ( + kwargs["auth"][0] != "user" or kwargs["auth"][1] != "password"): + mock_resp.status_code = 401 + elif kwargs["headers"].get("X-JFrog-Art-Api", None) and kwargs["headers"][ + "X-JFrog-Art-Api"] != "apikey": + mock_resp.status_code = 401 + buildinfo = json.load(data) + if not buildinfo["name"] == "MyBuildInfo" or not buildinfo["number"] == "42": + mock_resp.status_code = 400 + mock_resp.content = None + return mock_resp + + def _test_buildinfo(self, client, user_channel): + conanfile = textwrap.dedent(""" + from conans import ConanFile, load + import os + class Pkg(ConanFile): + {requires} + exports_sources = "myfile.txt" + keep_imports = True + def imports(self): + self.copy("myfile.txt", folder=True) + def package(self): + self.copy("*myfile.txt") + """) + client.save({"PkgA/conanfile.py": conanfile.format(requires=""), + "PkgA/myfile.txt": "HelloA"}) + client.run("create PkgA PkgA/0.1@{}".format(user_channel)) + + client.save({"PkgB/conanfile.py": conanfile.format( + requires='requires = "PkgA/0.1@{}"'.format(user_channel)), + "PkgB/myfile.txt": "HelloB"}) + client.run("create PkgB PkgB/0.1@{}".format(user_channel)) + + client.save({"PkgC/conanfile.py": conanfile.format( + requires='requires = "PkgA/0.1@{}"'.format(user_channel)), + "PkgC/myfile.txt": "HelloC"}) + client.run("create PkgC PkgC/0.1@{}".format(user_channel)) + + client.save({"PkgD/conanfile.py": conanfile.format( + requires='requires = "PkgC/0.1@{0}", "PkgB/0.1@{0}"'.format(user_channel)), + "PkgD/myfile.txt": "HelloD"}) + + client.run("create PkgD PkgD/0.1@{}".format(user_channel)) + client.run("graph lock PkgD/0.1@{}".format(user_channel)) + + client.run("create PkgA PkgA/0.2@{} --lockfile".format(user_channel)) + + shutil.copy(os.path.join(client.current_folder, "conan.lock"), + os.path.join(client.current_folder, "temp.lock")) + + client.run("create PkgB PkgB/0.1@{} --lockfile --build missing".format(user_channel)) + client.run("upload * --all --confirm -r default") + + sys.argv = ["conan_build_info", "--v2", "start", "MyBuildName", "42"] + run() + sys.argv = ["conan_build_info", "--v2", "create", + os.path.join(client.current_folder, "buildinfo1.json"), "--lockfile", + os.path.join(client.current_folder, LOCKFILE)] + run() + + shutil.copy(os.path.join(client.current_folder, "temp.lock"), + os.path.join(client.current_folder, "conan.lock")) + + client.run("create PkgC PkgC/0.1@{} --lockfile --build missing".format(user_channel)) + client.run("upload * --all --confirm -r default") + + sys.argv = ["conan_build_info", "--v2", "create", + os.path.join(client.current_folder, "buildinfo2.json"), "--lockfile", + os.path.join(client.current_folder, LOCKFILE)] + run() + + user_channel = "@" + user_channel if len(user_channel) > 2 else user_channel + with open(os.path.join(client.current_folder, "buildinfo1.json")) as f: + buildinfo = json.load(f) + self.assertEqual(buildinfo["name"], "MyBuildName") + self.assertEqual(buildinfo["number"], "42") + ids_list = [item["id"] for item in buildinfo["modules"]] + self.assertTrue("PkgB/0.1{}".format(user_channel) in ids_list) + self.assertTrue("PkgB/0.1{}:09f152eb7b3e0a6e15a2a3f464245864ae8f8644".format( + user_channel) in ids_list) + + sys.argv = ["conan_build_info", "--v2", "update", + os.path.join(client.current_folder, "buildinfo1.json"), + os.path.join(client.current_folder, "buildinfo2.json"), + "--output-file", os.path.join(client.current_folder, "mergedbuildinfo.json")] + run() + + with open(os.path.join(client.current_folder, "mergedbuildinfo.json")) as f: + buildinfo = json.load(f) + self.assertEqual(buildinfo["name"], "MyBuildName") + self.assertEqual(buildinfo["number"], "42") + ids_list = [item["id"] for item in buildinfo["modules"]] + self.assertTrue("PkgC/0.1{}".format(user_channel) in ids_list) + self.assertTrue("PkgB/0.1{}".format(user_channel) in ids_list) + self.assertTrue("PkgC/0.1{}:09f152eb7b3e0a6e15a2a3f464245864ae8f8644".format( + user_channel) in ids_list) + self.assertTrue("PkgB/0.1{}:09f152eb7b3e0a6e15a2a3f464245864ae8f8644".format( + user_channel) in ids_list) + + sys.argv = ["conan_build_info", "--v2", "publish", + os.path.join(client.current_folder, "mergedbuildinfo.json"), "--url", + "http://fakeurl:8081/artifactory", "--user", "user", "--password", "password"] + run() + sys.argv = ["conan_build_info", "--v2", "publish", + os.path.join(client.current_folder, "mergedbuildinfo.json"), "--url", + "http://fakeurl:8081/artifactory", "--apikey", "apikey"] + run() + + sys.argv = ["conan_build_info", "--v2", "stop"] + run() + + @patch("conans.build_info.build_info.get_conan_user_home") + @patch("conans.build_info.build_info.ClientCache") + @patch("conans.build_info.build_info.requests.put", new=mock_response) + def test_build_info_create_update_publish(self, mock_cache, user_home_mock): + base_folder = temp_folder(True) + cache_folder = os.path.join(base_folder, ".conan") + servers = {"default": TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], + users={"lasote": "mypass"})} + client = TestClient(servers=servers, users={"default": [("lasote", "mypass")]}, + cache_folder=cache_folder) + + mock_cache.return_value = client.cache + user_home_mock.return_value = base_folder + user_channels = ["", "user/channel"] + for user_channel in user_channels: + self._test_buildinfo(client, user_channel) diff --git a/conans/test/unittests/client/cmd/conan_build_info_test.py b/conans/test/unittests/client/cmd/conan_build_info_test.py new file mode 100644 index 00000000000..eb1ae18baae --- /dev/null +++ b/conans/test/unittests/client/cmd/conan_build_info_test.py @@ -0,0 +1,499 @@ +import json +import os +import textwrap +import unittest + +from conans.build_info.build_info import update_build_info +from conans.test.utils.test_files import temp_folder +from conans.tools import save + + +class BuildInfoTest(unittest.TestCase): + buildinfo1 = textwrap.dedent(""" + { + "version": "1.0.1", + "name": "MyBuildName", + "number": "42", + "type": "GENERIC", + "started": "2019-10-29T10:41:25.000Z", + "buildAgent": { + "name": "Conan Client", + "version": "1.X" + }, + "modules": [ + { + "id": "PkgB/0.1@user/channel", + "artifacts": [ + { + "sha1": "aba8527a2c4fc142cf5262298824d3680ecb057f", + "md5": "aad124317706ef90df47686329be8e2b", + "name": "conan_sources.tgz" + }, + { + "sha1": "a058b1a9366a361d71ea5d67997009f7200de6e1", + "md5": "a73be4ec0c7301d2ea2dacc873df5483", + "name": "conanfile.py" + }, + { + "sha1": "6cbda4a7286d9bb37eb3a6c97b150ed4939f4095", + "md5": "67abd07526f4abdf24f88f443c907f78", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "def7797033b5b46ca063aaaf21dc7a9c1b93a35a", + "md5": "89b684b95f6f5c7a8e2fda664be22c5a", + "id": "PkgA/0.2@user/channel :: conan_sources.tgz" + }, + { + "sha1": "7bd4da1c70ca29637b159a0131a8b886cfaeeb27", + "md5": "00dbccdd251aa5652df8886cf153d2d6", + "id": "PkgA/0.2@user/channel :: conanfile.py" + }, + { + "sha1": "7ca1713befc9eabcfaaa244133e3e173307705f4", + "md5": "d723dfc35eb5a121e401818fdb43b210", + "id": "PkgA/0.2@user/channel :: conanmanifest.txt" + } + ] + }, + { + "id": "PkgB/0.1@user/channel:09f152eb7b3e0a6e15a2a3f464245864ae8f8644", + "artifacts": [ + { + "sha1": "45f961804e3bcc5267a2f6d130b4dcc16e2379ee", + "md5": "d4f703971717722bd84c24eccf50b9fd", + "name": "conan_package.tgz" + }, + { + "sha1": "9525339890e3b484d5e7d8f351957b6c2a28147f", + "md5": "fd6b4a992aa1254fa5a404888ed8c7ce", + "name": "conaninfo.txt" + }, + { + "sha1": "f8f2795005c8fbfbcddae7224888d66a8f47f533", + "md5": "c18ffa171f4df3ab4af24dc443320a5a", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "a96d326d2449a103a4f9e6d81018ffd411b3f4a1", + "md5": "43c402f3ad0cc9dfa89c5be37bf9b7e5", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conan_package.tgz" + }, + { + "sha1": "2f452380f6ec5db0baab369d0bc4286793710ca3", + "md5": "95adc888e92d1a888454fae2093c0862", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conaninfo.txt" + }, + { + "sha1": "fe0b6d9343648ae2b60d5c6c4ce765291cc278fb", + "md5": "2702b1656a7318c01112b90cca875867", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conanmanifest.txt" + } + ] + }, + { + "id": "PkgA/0.2@user/channel", + "artifacts": [ + { + "sha1": "def7797033b5b46ca063aaaf21dc7a9c1b93a35a", + "md5": "89b684b95f6f5c7a8e2fda664be22c5a", + "name": "conan_sources.tgz" + }, + { + "sha1": "7bd4da1c70ca29637b159a0131a8b886cfaeeb27", + "md5": "00dbccdd251aa5652df8886cf153d2d6", + "name": "conanfile.py" + }, + { + "sha1": "7ca1713befc9eabcfaaa244133e3e173307705f4", + "md5": "d723dfc35eb5a121e401818fdb43b210", + "name": "conanmanifest.txt" + } + ], + "dependencies": [] + }, + { + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9", + "artifacts": [ + { + "sha1": "a96d326d2449a103a4f9e6d81018ffd411b3f4a1", + "md5": "43c402f3ad0cc9dfa89c5be37bf9b7e5", + "name": "conan_package.tgz" + }, + { + "sha1": "2f452380f6ec5db0baab369d0bc4286793710ca3", + "md5": "95adc888e92d1a888454fae2093c0862", + "name": "conaninfo.txt" + }, + { + "sha1": "fe0b6d9343648ae2b60d5c6c4ce765291cc278fb", + "md5": "2702b1656a7318c01112b90cca875867", + "name": "conanmanifest.txt" + } + ], + "dependencies": [] + } + ] + }""") + + buildinfo2 = textwrap.dedent(""" + { + "version": "1.0.1", + "name": "MyBuildName", + "number": "42", + "type": "GENERIC", + "started": "2019-10-29T10:41:25.000Z", + "buildAgent": { + "name": "Conan Client", + "version": "1.X" + }, + "modules": [ + { + "id": "PkgC/0.1@user/channel", + "artifacts": [ + { + "sha1": "410b7df1fd1483a5a7b4c47e67822fc1e3dd533b", + "md5": "461fbd5d7e66ce86b8e56fb2524970dc", + "name": "conan_sources.tgz" + }, + { + "sha1": "a058b1a9366a361d71ea5d67997009f7200de6e1", + "md5": "a73be4ec0c7301d2ea2dacc873df5483", + "name": "conanfile.py" + }, + { + "sha1": "594ff68dadb4cbeb36ff724286032698098b41e7", + "md5": "19052f117ce1513c3a815f41fd704c24", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "def7797033b5b46ca063aaaf21dc7a9c1b93a35a", + "md5": "89b684b95f6f5c7a8e2fda664be22c5a", + "id": "PkgA/0.2@user/channel :: conan_sources.tgz" + }, + { + "sha1": "7bd4da1c70ca29637b159a0131a8b886cfaeeb27", + "md5": "00dbccdd251aa5652df8886cf153d2d6", + "id": "PkgA/0.2@user/channel :: conanfile.py" + }, + { + "sha1": "7ca1713befc9eabcfaaa244133e3e173307705f4", + "md5": "d723dfc35eb5a121e401818fdb43b210", + "id": "PkgA/0.2@user/channel :: conanmanifest.txt" + } + ] + }, + { + "id": "PkgC/0.1@user/channel:09f152eb7b3e0a6e15a2a3f464245864ae8f8644", + "artifacts": [ + { + "sha1": "0b6a6755369820f66d6a858d3b44775fb1b38f54", + "md5": "c1f3a9ff4ee80ab5e5492c1c381dff56", + "name": "conan_package.tgz" + }, + { + "sha1": "6bce988c0cfc1d17588c0fddac573066afd8d26d", + "md5": "bde279efd0a24162425017d937fe8484", + "name": "conaninfo.txt" + }, + { + "sha1": "6a85f6893f316433cd935abad31c4daf80d09884", + "md5": "5996a968f13f4e4722d24d9d98ed0923", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "a96d326d2449a103a4f9e6d81018ffd411b3f4a1", + "md5": "43c402f3ad0cc9dfa89c5be37bf9b7e5", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conan_package.tgz" + }, + { + "sha1": "2f452380f6ec5db0baab369d0bc4286793710ca3", + "md5": "95adc888e92d1a888454fae2093c0862", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conaninfo.txt" + }, + { + "sha1": "fe0b6d9343648ae2b60d5c6c4ce765291cc278fb", + "md5": "2702b1656a7318c01112b90cca875867", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conanmanifest.txt" + } + ] + }, + { + "id": "PkgA/0.2@user/channel", + "artifacts": [ + { + "sha1": "def7797033b5b46ca063aaaf21dc7a9c1b93a35a", + "md5": "89b684b95f6f5c7a8e2fda664be22c5a", + "name": "conan_sources.tgz" + }, + { + "sha1": "7bd4da1c70ca29637b159a0131a8b886cfaeeb27", + "md5": "00dbccdd251aa5652df8886cf153d2d6", + "name": "conanfile.py" + }, + { + "sha1": "7ca1713befc9eabcfaaa244133e3e173307705f4", + "md5": "d723dfc35eb5a121e401818fdb43b210", + "name": "conanmanifest.txt" + } + ], + "dependencies": [] + }, + { + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9", + "artifacts": [ + { + "sha1": "a96d326d2449a103a4f9e6d81018ffd411b3f4a1", + "md5": "43c402f3ad0cc9dfa89c5be37bf9b7e5", + "name": "conan_package.tgz" + }, + { + "sha1": "2f452380f6ec5db0baab369d0bc4286793710ca3", + "md5": "95adc888e92d1a888454fae2093c0862", + "name": "conaninfo.txt" + }, + { + "sha1": "fe0b6d9343648ae2b60d5c6c4ce765291cc278fb", + "md5": "2702b1656a7318c01112b90cca875867", + "name": "conanmanifest.txt" + } + ], + "dependencies": [] + } + ] + }""") + + result = textwrap.dedent(""" + { + "version": "1.0.1", + "name": "MyBuildName", + "number": "42", + "type": "GENERIC", + "started": "2019-10-29T10:41:25.000Z", + "buildAgent": { + "name": "Conan Client", + "version": "1.X" + }, + "modules": [ + { + "id": "PkgB/0.1@user/channel", + "artifacts": [ + { + "sha1": "aba8527a2c4fc142cf5262298824d3680ecb057f", + "md5": "aad124317706ef90df47686329be8e2b", + "name": "conan_sources.tgz" + }, + { + "sha1": "a058b1a9366a361d71ea5d67997009f7200de6e1", + "md5": "a73be4ec0c7301d2ea2dacc873df5483", + "name": "conanfile.py" + }, + { + "sha1": "6cbda4a7286d9bb37eb3a6c97b150ed4939f4095", + "md5": "67abd07526f4abdf24f88f443c907f78", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "def7797033b5b46ca063aaaf21dc7a9c1b93a35a", + "md5": "89b684b95f6f5c7a8e2fda664be22c5a", + "id": "PkgA/0.2@user/channel :: conan_sources.tgz" + }, + { + "sha1": "7bd4da1c70ca29637b159a0131a8b886cfaeeb27", + "md5": "00dbccdd251aa5652df8886cf153d2d6", + "id": "PkgA/0.2@user/channel :: conanfile.py" + }, + { + "sha1": "7ca1713befc9eabcfaaa244133e3e173307705f4", + "md5": "d723dfc35eb5a121e401818fdb43b210", + "id": "PkgA/0.2@user/channel :: conanmanifest.txt" + } + ] + }, + { + "id": "PkgB/0.1@user/channel:09f152eb7b3e0a6e15a2a3f464245864ae8f8644", + "artifacts": [ + { + "sha1": "45f961804e3bcc5267a2f6d130b4dcc16e2379ee", + "md5": "d4f703971717722bd84c24eccf50b9fd", + "name": "conan_package.tgz" + }, + { + "sha1": "9525339890e3b484d5e7d8f351957b6c2a28147f", + "md5": "fd6b4a992aa1254fa5a404888ed8c7ce", + "name": "conaninfo.txt" + }, + { + "sha1": "f8f2795005c8fbfbcddae7224888d66a8f47f533", + "md5": "c18ffa171f4df3ab4af24dc443320a5a", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "a96d326d2449a103a4f9e6d81018ffd411b3f4a1", + "md5": "43c402f3ad0cc9dfa89c5be37bf9b7e5", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conan_package.tgz" + }, + { + "sha1": "2f452380f6ec5db0baab369d0bc4286793710ca3", + "md5": "95adc888e92d1a888454fae2093c0862", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conaninfo.txt" + }, + { + "sha1": "fe0b6d9343648ae2b60d5c6c4ce765291cc278fb", + "md5": "2702b1656a7318c01112b90cca875867", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conanmanifest.txt" + } + ] + }, + { + "id": "PkgA/0.2@user/channel", + "artifacts": [ + { + "sha1": "def7797033b5b46ca063aaaf21dc7a9c1b93a35a", + "md5": "89b684b95f6f5c7a8e2fda664be22c5a", + "name": "conan_sources.tgz" + }, + { + "sha1": "7bd4da1c70ca29637b159a0131a8b886cfaeeb27", + "md5": "00dbccdd251aa5652df8886cf153d2d6", + "name": "conanfile.py" + }, + { + "sha1": "7ca1713befc9eabcfaaa244133e3e173307705f4", + "md5": "d723dfc35eb5a121e401818fdb43b210", + "name": "conanmanifest.txt" + } + ], + "dependencies": [] + }, + { + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9", + "artifacts": [ + { + "sha1": "a96d326d2449a103a4f9e6d81018ffd411b3f4a1", + "md5": "43c402f3ad0cc9dfa89c5be37bf9b7e5", + "name": "conan_package.tgz" + }, + { + "sha1": "2f452380f6ec5db0baab369d0bc4286793710ca3", + "md5": "95adc888e92d1a888454fae2093c0862", + "name": "conaninfo.txt" + }, + { + "sha1": "fe0b6d9343648ae2b60d5c6c4ce765291cc278fb", + "md5": "2702b1656a7318c01112b90cca875867", + "name": "conanmanifest.txt" + } + ], + "dependencies": [] + }, + { + "id": "PkgC/0.1@user/channel", + "artifacts": [ + { + "sha1": "410b7df1fd1483a5a7b4c47e67822fc1e3dd533b", + "md5": "461fbd5d7e66ce86b8e56fb2524970dc", + "name": "conan_sources.tgz" + }, + { + "sha1": "a058b1a9366a361d71ea5d67997009f7200de6e1", + "md5": "a73be4ec0c7301d2ea2dacc873df5483", + "name": "conanfile.py" + }, + { + "sha1": "594ff68dadb4cbeb36ff724286032698098b41e7", + "md5": "19052f117ce1513c3a815f41fd704c24", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "def7797033b5b46ca063aaaf21dc7a9c1b93a35a", + "md5": "89b684b95f6f5c7a8e2fda664be22c5a", + "id": "PkgA/0.2@user/channel :: conan_sources.tgz" + }, + { + "sha1": "7bd4da1c70ca29637b159a0131a8b886cfaeeb27", + "md5": "00dbccdd251aa5652df8886cf153d2d6", + "id": "PkgA/0.2@user/channel :: conanfile.py" + }, + { + "sha1": "7ca1713befc9eabcfaaa244133e3e173307705f4", + "md5": "d723dfc35eb5a121e401818fdb43b210", + "id": "PkgA/0.2@user/channel :: conanmanifest.txt" + } + ] + }, + { + "id": "PkgC/0.1@user/channel:09f152eb7b3e0a6e15a2a3f464245864ae8f8644", + "artifacts": [ + { + "sha1": "0b6a6755369820f66d6a858d3b44775fb1b38f54", + "md5": "c1f3a9ff4ee80ab5e5492c1c381dff56", + "name": "conan_package.tgz" + }, + { + "sha1": "6bce988c0cfc1d17588c0fddac573066afd8d26d", + "md5": "bde279efd0a24162425017d937fe8484", + "name": "conaninfo.txt" + }, + { + "sha1": "6a85f6893f316433cd935abad31c4daf80d09884", + "md5": "5996a968f13f4e4722d24d9d98ed0923", + "name": "conanmanifest.txt" + } + ], + "dependencies": [ + { + "sha1": "a96d326d2449a103a4f9e6d81018ffd411b3f4a1", + "md5": "43c402f3ad0cc9dfa89c5be37bf9b7e5", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conan_package.tgz" + }, + { + "sha1": "2f452380f6ec5db0baab369d0bc4286793710ca3", + "md5": "95adc888e92d1a888454fae2093c0862", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conaninfo.txt" + }, + { + "sha1": "fe0b6d9343648ae2b60d5c6c4ce765291cc278fb", + "md5": "2702b1656a7318c01112b90cca875867", + "id": "PkgA/0.2@user/channel:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 :: conanmanifest.txt" + } + ] + } + ] + }""") + + def update_build_info_test(self): + tmp_dir = temp_folder() + file1 = os.path.join(tmp_dir, "buildinfo1.json") + file2 = os.path.join(tmp_dir, "buildinfo2.json") + outfile = os.path.join(tmp_dir, "mergedbuildinfo.json") + save(file1, self.buildinfo1) + save(file2, self.buildinfo2) + update_build_info([file1, file2], outfile) + with open(outfile, "r") as json_data: + mergedinfo = json.load(json_data) + res_json = json.loads(self.result) + + self.assertEqual(mergedinfo["version"], res_json["version"]) + self.assertEqual(mergedinfo["name"], res_json["name"]) + self.assertEqual(mergedinfo["number"], res_json["number"]) + self.assertEqual(mergedinfo["type"], res_json["type"]) + self.assertEqual(mergedinfo["started"], res_json["started"]) + self.assertDictEqual(mergedinfo["buildAgent"], res_json["buildAgent"]) + for index in range(2): + self.assertEqual(mergedinfo["modules"][index]["id"], + res_json["modules"][index]["id"])