Repository: conan-io/conan

Issue:
add support for linux on s390/s390x
https://en.wikipedia.org/wiki/Linux_on_z_Systems

In our CI we are running "normal" Linux distributions like Ubuntu and Red Hat on s390/s390x.

To publish and fetch packages, we would need support for s390 (31bit, yes, gcc will need -m31) and s390x (64bit).

```
>>> import platform
>>> platform.system()
'Linux'
>>> platform.machine()
's390x'
>>> platform.processor()
's390x'
>>>
```

Discussion:

Patch:
diff --git a/LICENSE.md b/LICENSE.md
index c040a7091..1ff9c5884 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2018 JFrog LTD
+Copyright (c) 2019 JFrog LTD
 
 
 
diff --git a/conans/client/build/compiler_flags.py b/conans/client/build/compiler_flags.py
index c3337eced..d6e5955c5 100644
--- a/conans/client/build/compiler_flags.py
+++ b/conans/client/build/compiler_flags.py
@@ -31,10 +31,12 @@ def architecture_flag(compiler, arch):
         return ""
 
     if str(compiler) in ['gcc', 'apple-clang', 'clang', 'sun-cc']:
-        if str(arch) in ['x86_64', 'sparcv9']:
+        if str(arch) in ['x86_64', 'sparcv9', 's390x']:
             return '-m64'
         elif str(arch) in ['x86', 'sparc']:
             return '-m32'
+        elif str(arch) in ['s390']:
+            return '-m31'
     return ""
 
 
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
index 7bd0e0840..6c040e15c 100644
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -18,12 +18,14 @@ from conans.util.log import logger
 
 
 def export_alias(package_layout, target_ref, output, revisions_enabled):
+    revision_mode = "hash"
     conanfile = """
 from conans import ConanFile
 
 class AliasConanfile(ConanFile):
     alias = "%s"
-""" % target_ref.full_repr()
+    revision_mode = "%s"
+""" % (target_ref.full_repr(), revision_mode)
 
     save(package_layout.conanfile(), conanfile)
     digest = FileTreeManifest.create(package_layout.export())
@@ -31,7 +33,8 @@ class AliasConanfile(ConanFile):
 
     # Create the metadata for the alias
     _update_revision_in_metadata(package_layout=package_layout, revisions_enabled=revisions_enabled,
-                                 output=output, path=None, digest=digest)
+                                 output=output, path=None, digest=digest,
+                                 revision_mode=revision_mode)
 
 
 def check_casing_conflict(cache, ref):
@@ -97,7 +100,8 @@ def cmd_export(package_layout, conanfile_path, conanfile, keep_source, revisions
                                             revisions_enabled=revisions_enabled,
                                             output=output,
                                             path=os.path.dirname(conanfile_path),
-                                            digest=digest)
+                                            digest=digest,
+                                            revision_mode=conanfile.revision_mode)
 
     # FIXME: Conan 2.0 Clear the registry entry if the recipe has changed
     source_folder = package_layout.source()
@@ -239,19 +243,30 @@ def _detect_scm_revision(path):
     return repo_obj.get_revision(), repo_type, repo_obj.is_pristine()
 
 
-def _update_revision_in_metadata(package_layout, revisions_enabled, output, path, digest):
+def _update_revision_in_metadata(package_layout, revisions_enabled, output, path, digest,
+                                 revision_mode):
+    if revision_mode not in ["scm", "hash"]:
+        raise ConanException("Revision mode should be one of 'hash' (default) or 'scm'")
 
-    scm_revision_detected, repo_type, is_pristine = _detect_scm_revision(path)
-    revision = scm_revision_detected or digest.summary_hash
-    if revisions_enabled:
-        if scm_revision_detected:
-            output.info("Using {} commit as the recipe"
-                        " revision: {} ".format(repo_type, revision))
-            if not is_pristine:
-                output.warn("Repo status is not pristine: there might be modified files")
-        else:
+    # Use the proper approach depending on 'revision_mode'
+    if revision_mode == "hash":
+        revision = digest.summary_hash
+        if revisions_enabled:
             output.info("Using the exported files summary hash as the recipe"
                         " revision: {} ".format(revision))
+    else:
+        rev_detected, repo_type, is_pristine = _detect_scm_revision(path)
+        if not rev_detected:
+            raise ConanException("Cannot detect revision using '{}' mode"
+                                 " from repository at '{}'".format(revision_mode, path))
+
+        revision = rev_detected
+
+        if revisions_enabled:
+            output.info("Using %s commit as the recipe revision: %s" % (repo_type, revision))
+            if not is_pristine:
+                output.warn("Repo status is not pristine: there might be modified files")
+
     with package_layout.update_metadata() as metadata:
         metadata.recipe.revision = revision
 
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
index b52d3fdf4..a1a7d3faa 100644
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -4,6 +4,7 @@ import tarfile
 import time
 from collections import defaultdict
 
+from conans.client.remote_manager import is_package_snapshot_complete
 from conans.client.source import complete_recipe_sources
 from conans.errors import ConanException, NotFoundException
 from conans.model.manifest import gather_files, FileTreeManifest
@@ -352,23 +353,20 @@ class CmdUpload(object):
         return files_to_upload, deleted
 
     def _package_files_to_upload(self, pref, policy, the_files, remote):
-        # Get the remote snapshot
         remote_snapshot = self._remote_manager.get_package_snapshot(pref, remote)
 
         if remote_snapshot:
+            if not is_package_snapshot_complete(remote_snapshot):
+                return the_files, set([])
             remote_manifest, _ = self._remote_manager.get_package_manifest(pref, remote)
             local_manifest = FileTreeManifest.loads(load(the_files["conanmanifest.txt"]))
-
             if remote_manifest == local_manifest:
                 return None, None
-
             if policy == UPLOAD_POLICY_NO_OVERWRITE:
-                raise ConanException("Local package is different from the remote package. "
-                                     "Forbidden overwrite.")
-        files_to_upload = the_files
+                raise ConanException("Local package is different from the remote package. Forbidden "
+                                     "overwrite.")
         deleted = set(remote_snapshot).difference(the_files)
-
-        return files_to_upload, deleted
+        return the_files, deleted
 
     def _upload_recipe_end_msg(self, ref, remote):
         msg = "Uploaded conan recipe '%s' to '%s'" % (str(ref), remote.name)
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index c3706f695..80132bfa8 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -300,8 +300,8 @@ class ConanAPIV1(object):
         if not attributes:
             attributes = ['name', 'version', 'url', 'homepage', 'license', 'author',
                           'description', 'topics', 'generators', 'exports', 'exports_sources',
-                          'short_paths', 'apply_env', 'build_policy', 'settings', 'options',
-                          'default_options']
+                          'short_paths', 'apply_env', 'build_policy', 'revision_mode', 'settings',
+                          'options', 'default_options']
         for attribute in attributes:
             try:
                 attr = getattr(conanfile, attribute)
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index d65109193..e25878b1d 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -13,12 +13,12 @@ from conans.util.files import load
 default_settings_yml = """
 # Only for cross building, 'os_build/arch_build' is the system that runs Conan
 os_build: [Windows, WindowsStore, Linux, Macos, FreeBSD, SunOS]
-arch_build: [x86, x86_64, ppc32, ppc64le, ppc64, armv5el, armv5hf, armv6, armv7, armv7hf, armv7s, armv7k, armv8, armv8_32, armv8.3, sparc, sparcv9, mips, mips64, avr]
+arch_build: [x86, x86_64, ppc32, ppc64le, ppc64, armv5el, armv5hf, armv6, armv7, armv7hf, armv7s, armv7k, armv8, armv8_32, armv8.3, sparc, sparcv9, mips, mips64, avr, s390, s390x]
 
 # Only for building cross compilation tools, 'os_target/arch_target' is the system for
 # which the tools generate code
 os_target: [Windows, Linux, Macos, Android, iOS, watchOS, tvOS, FreeBSD, SunOS, Arduino]
-arch_target: [x86, x86_64, ppc32, ppc64le, ppc64, armv5el, armv5hf, armv6, armv7, armv7hf, armv7s, armv7k, armv8, armv8_32, armv8.3, sparc, sparcv9, mips, mips64, avr]
+arch_target: [x86, x86_64, ppc32, ppc64le, ppc64, armv5el, armv5hf, armv6, armv7, armv7hf, armv7s, armv7k, armv8, armv8_32, armv8.3, sparc, sparcv9, mips, mips64, avr, s390, s390x]
 
 # Rest of the settings are "host" settings:
 # - For native building/cross building: Where the library/program will run.
@@ -43,7 +43,7 @@ os:
     SunOS:
     Arduino:
         board: ANY
-arch: [x86, x86_64, ppc32, ppc64le, ppc64, armv5el, armv5hf, armv6, armv7, armv7hf, armv7s, armv7k, armv8, armv8_32, armv8.3, sparc, sparcv9, mips, mips64, avr]
+arch: [x86, x86_64, ppc32, ppc64le, ppc64, armv5el, armv5hf, armv6, armv7, armv7hf, armv7s, armv7k, armv8, armv8_32, armv8.3, sparc, sparcv9, mips, mips64, avr, s390, s390x]
 compiler:
     sun-cc:
         version: ["5.10", "5.11", "5.12", "5.13", "5.14"]
diff --git a/conans/client/generators/json_generator.py b/conans/client/generators/json_generator.py
index 844d52feb..21336d8fc 100644
--- a/conans/client/generators/json_generator.py
+++ b/conans/client/generators/json_generator.py
@@ -46,6 +46,8 @@ class JsonGenerator(Generator):
         for depname, cpp_info in self.deps_build_info.dependencies:
             serialized_info = serialize_cpp_info(cpp_info)
             serialized_info["name"] = depname
+            for cfg, cfg_cpp_info in cpp_info.configs.items():
+                serialized_info.setdefault("configs", {})[cfg] = serialize_cpp_info(cfg_cpp_info)
             res.append(serialized_info)
         return res
 
diff --git a/conans/client/graph/grapher.py b/conans/client/graph/grapher.py
index a295d5c05..77b7340ef 100644
--- a/conans/client/graph/grapher.py
+++ b/conans/client/graph/grapher.py
@@ -63,6 +63,8 @@ class ConanHTMLGrapher(object):
                                ("author", conanfile.author),
                                ("topics", str(conanfile.topics))]:
                 if data:
+                    if isinstance(data, (tuple, list)):
+                        data = ', '.join(data)
                     data = data.replace("'", '"')
                     fulllabel.append("<li><b>%s</b>: %s</li>" % (name, data))
 
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index 713eacaf0..8aea2cc31 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -11,7 +11,8 @@ from conans.client.source import merge_directories
 from conans.errors import ConanConnectionError, ConanException, NotFoundException, \
     NoRestV2Available, PackageNotFoundException
 from conans.paths import EXPORT_SOURCES_DIR_OLD, \
-    EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, rm_conandir
+    EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, rm_conandir, CONANINFO, \
+    CONAN_MANIFEST
 from conans.search.search import filter_packages
 from conans.util import progress_bar
 from conans.util.env_reader import get_env
@@ -129,6 +130,9 @@ class RemoteManager(object):
         t1 = time.time()
         try:
             pref = self._resolve_latest_pref(pref, remote)
+            snapshot = self._call_remote(remote, "get_package_snapshot", pref)
+            if not is_package_snapshot_complete(snapshot):
+                raise PackageNotFoundException(pref)
             zipped_files = self._call_remote(remote, "get_package", pref, dest_folder)
 
             with self._cache.package_layout(pref.ref).update_metadata() as metadata:
@@ -241,6 +245,15 @@ class RemoteManager(object):
             raise ConanException(exc, remote=remote)
 
 
+def is_package_snapshot_complete(snapshot):
+    integrity = True
+    for keyword in ["conaninfo", "conanmanifest", "conan_package"]:
+        if not any(keyword in key for key in snapshot):
+            integrity = False
+            break
+    return integrity
+
+
 def check_compressed_files(tgz_name, files):
     bare_name = os.path.splitext(tgz_name)[0]
     for f in files:
diff --git a/conans/client/rest/rest_client_v1.py b/conans/client/rest/rest_client_v1.py
index 4db42a8bb..9c3e1f06d 100644
--- a/conans/client/rest/rest_client_v1.py
+++ b/conans/client/rest/rest_client_v1.py
@@ -142,9 +142,9 @@ class RestV1Methods(RestCommonMethods):
         t1 = time.time()
         failed = []
         uploader = Uploader(self.requester, output, self.verify_ssl)
-        # Take advantage of filenames ordering, so that conan_package.tgz and conan_export.tgz
-        # can be < conanfile, conaninfo, and sent always the last, so smaller files go first
-        for filename, resource_url in sorted(file_urls.items(), reverse=True):
+        # conan_package.tgz and conan_export.tgz are uploaded first to avoid uploading conaninfo.txt
+        # or conanamanifest.txt with missing files due to a network failure
+        for filename, resource_url in sorted(file_urls.items()):
             output.rewrite_line("Uploading %s" % filename)
             auth, dedup = self._file_server_capabilities(resource_url)
             try:
diff --git a/conans/client/rest/rest_client_v2.py b/conans/client/rest/rest_client_v2.py
index 5362fbcbd..c693fa5d9 100644
--- a/conans/client/rest/rest_client_v2.py
+++ b/conans/client/rest/rest_client_v2.py
@@ -174,9 +174,9 @@ class RestV2Methods(RestCommonMethods):
         t1 = time.time()
         failed = []
         uploader = Uploader(self.requester, self._output, self.verify_ssl)
-        # Take advantage of filenames ordering, so that conan_package.tgz and conan_export.tgz
-        # can be < conanfile, conaninfo, and sent always the last, so smaller files go first
-        for filename in sorted(files, reverse=True):
+        # conan_package.tgz and conan_export.tgz are uploaded first to avoid uploading conaninfo.txt
+        # or conanamanifest.txt with missing files due to a network failure
+        for filename in sorted(files):
             self._output.rewrite_line("Uploading %s" % filename)
             resource_url = urls[filename]
             try:
diff --git a/conans/client/tools/oss.py b/conans/client/tools/oss.py
index e61f7cc24..96331cebd 100644
--- a/conans/client/tools/oss.py
+++ b/conans/client/tools/oss.py
@@ -70,6 +70,10 @@ def detected_architecture():
         return "armv7"
     elif "arm" in machine:
         return "armv6"
+    elif "s390x" in machine:
+        return "s390x"
+    elif "s390" in machine:
+        return "s390"
 
     return None
 
@@ -390,6 +394,10 @@ def get_gnu_triplet(os_, arch, compiler=None):
             machine = "sparc64"
         elif "sparc" in arch:
             machine = "sparc"
+        elif "s390x" in arch:
+            machine = "s390x-ibm"
+        elif "s390" in arch:
+            machine = "s390-ibm"
 
     if machine is None:
         raise ConanException("Unknown '%s' machine, Conan doesn't know how to "
diff --git a/conans/client/tools/win.py b/conans/client/tools/win.py
index f19fd2859..da68c0902 100644
--- a/conans/client/tools/win.py
+++ b/conans/client/tools/win.py
@@ -252,14 +252,13 @@ def vswhere(all_=False, prerelease=False, products=None, requires=None, version=
                              "'products' or 'requires' parameter")
 
     installer_path = None
-    vswhere_in_path = which("vswhere")
     program_files = get_env("ProgramFiles(x86)") or get_env("ProgramFiles")
     if program_files:
         expected_path = os.path.join(program_files, "Microsoft Visual Studio", "Installer",
                                      "vswhere.exe")
         if os.path.isfile(expected_path):
             installer_path = expected_path
-    vswhere_path = installer_path or vswhere_in_path
+    vswhere_path = installer_path or which("vswhere")
 
     if not vswhere_path:
         raise ConanException("Cannot locate vswhere in 'Program Files'/'Program Files (x86)' "
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
index 4df47fea3..848ea67b2 100644
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -97,6 +97,7 @@ class ConanFile(object):
     exports = None
     exports_sources = None
     generators = ["txt"]
+    revision_mode = "hash"
 
     # Vars to control the build steps (build(), package())
     should_configure = True


Test patch:
diff --git a/conans/test/functional/command/export_test.py b/conans/test/functional/command/export_test.py
index 1e797d62f..14646bd28 100644
--- a/conans/test/functional/command/export_test.py
+++ b/conans/test/functional/command/export_test.py
@@ -1,5 +1,6 @@
 import os
 import stat
+import textwrap
 import unittest
 
 from parameterized import parameterized
@@ -9,6 +10,7 @@ from conans.model.ref import ConanFileReference
 from conans.paths import CONANFILE, CONAN_MANIFEST
 from conans.test.utils.cpp_test_files import cpp_hello_conan_files
 from conans.test.utils.tools import TestClient
+from conans.test.utils.tools import create_local_git_repo
 from conans.util.files import load, save
 
 
@@ -419,3 +421,44 @@ class OpenSSLConan(ConanFile):
                 save(file_path, content)
                 file_list.append(file_path)
         return file_list
+
+
+class ExportMetadataTest(unittest.TestCase):
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        
+        class Lib(ConanFile):
+            revision_mode = "{revision_mode}"
+    """)
+
+    summary_hash = "bfe8b4a6a2a74966c0c4e0b34705004a"
+
+    def test_revision_mode_hash(self):
+        t = TestClient()
+        t.save({'conanfile.py': self.conanfile.format(revision_mode="hash")})
+
+        ref = ConanFileReference.loads("name/version@user/channel")
+        t.run("export . {}".format(ref))
+
+        meta = t.cache.package_layout(ref, short_paths=False).load_metadata()
+        self.assertEqual(meta.recipe.revision, self.summary_hash)
+
+    def test_revision_mode_scm(self):
+        path, rev = create_local_git_repo(
+            files={'conanfile.py': self.conanfile.format(revision_mode="scm")})
+        t = TestClient(current_folder=path)
+
+        ref = ConanFileReference.loads("name/version@user/channel")
+        t.run("export . {}".format(ref))
+
+        meta = t.cache.package_layout(ref, short_paths=False).load_metadata()
+        self.assertEqual(meta.recipe.revision, rev)
+
+    def test_revision_mode_invalid(self):
+        conanfile = self.conanfile.format(revision_mode="auto")
+
+        t = TestClient()
+        t.save({'conanfile.py': conanfile})
+        ref = ConanFileReference.loads("name/version@user/channel")
+        t.run("export . {}".format(ref), assert_error=True)
+        self.assertIn("ERROR: Revision mode should be one of 'hash' (default) or 'scm'", t.out)
diff --git a/conans/test/functional/command/inspect_test.py b/conans/test/functional/command/inspect_test.py
index ae41a84ce..3aecaa2b2 100644
--- a/conans/test/functional/command/inspect_test.py
+++ b/conans/test/functional/command/inspect_test.py
@@ -83,6 +83,8 @@ class Pkg(ConanFile):
         self.assertIn("version: None", client.out)
         client.run("inspect . -a=settings")
         self.assertIn("settings: ('os', 'compiler', 'arch')", client.out)
+        client.run("inspect . -a=revision_mode")
+        self.assertIn("revision_mode: hash", client.out)
 
         client.run("inspect . -a=unexisting_attr", assert_error=True)
         self.assertIn("ERROR: 'Pkg' object has no attribute 'unexisting_attr'", client.out)
@@ -147,6 +149,7 @@ exports_sources: None
 short_paths: False
 apply_env: True
 build_policy: None
+revision_mode: hash
 settings: None
 options: None
 default_options: None
@@ -169,6 +172,7 @@ class Pkg(ConanFile):
     options = {"foo": [True, False], "bar": [True, False]}
     default_options = {"foo": True, "bar": False}
     _private = "Nothing"
+    revision_mode = "scm"
     def build(self):
         pass
 """
@@ -188,6 +192,7 @@ exports_sources: None
 short_paths: False
 apply_env: True
 build_policy: None
+revision_mode: scm
 settings: ('os', 'arch', 'build_type', 'compiler')
 options:
     bar: [True, False]
@@ -229,6 +234,7 @@ exports_sources: None
 short_paths: False
 apply_env: True
 build_policy: None
+revision_mode: hash
 settings: ('os', 'compiler', 'arch', 'build_type')
 options:
     386: [True, False]
@@ -276,6 +282,7 @@ exports_sources: None
 short_paths: False
 apply_env: True
 build_policy: None
+revision_mode: hash
 settings: ('os', 'arch', 'build_type', 'compiler')
 options:
     bar: [True, False]
@@ -302,6 +309,7 @@ exports_sources: None
 short_paths: False
 apply_env: True
 build_policy: None
+revision_mode: hash
 settings: ('os', 'arch', 'build_type', 'compiler')
 options:
     bar: [True, False]
@@ -309,4 +317,4 @@ options:
 default_options:
     bar: True
     foo: True
-""", client.out)
\ No newline at end of file
+""", client.out)
diff --git a/conans/test/functional/command/upload_complete_test.py b/conans/test/functional/command/upload_complete_test.py
index 0633daa58..66fbcff00 100644
--- a/conans/test/functional/command/upload_complete_test.py
+++ b/conans/test/functional/command/upload_complete_test.py
@@ -176,7 +176,7 @@ class UploadTest(unittest.TestCase):
         client.run("upload Hello* --confirm --retry 1 --retry-wait=1", assert_error=True)
         self.assertNotIn("Waiting 1 seconds to retry...", client.user_io.out)
         self.assertIn("ERROR: Execute upload again to retry upload the failed files: "
-                      "conanmanifest.txt. [Remote: default]", client.user_io.out)
+                      "conan_export.tgz. [Remote: default]", client.user_io.out)
 
         # Try with broken connection even with 10 retries
         client = self._get_client(TerribleConnectionUploader)
@@ -330,13 +330,13 @@ class TestConan(ConanFile):
                  if line.startswith("Uploading")]
         self.assertEqual(lines, ["Uploading to remote 'default':",
                                  "Uploading Hello/1.2.1@frodo/stable to remote 'default'",
-                                 "Uploading conanmanifest.txt",
-                                 "Uploading conanfile.py",
                                  "Uploading conan_export.tgz",
-                                 "Uploading package 1/1: myfakeid to 'default'",
+                                 "Uploading conanfile.py",
                                  "Uploading conanmanifest.txt",
-                                 "Uploading conaninfo.txt",
+                                 "Uploading package 1/1: myfakeid to 'default'",
                                  "Uploading conan_package.tgz",
+                                 "Uploading conaninfo.txt",
+                                 "Uploading conanmanifest.txt",
                                  ])
         if self.client.cache.config.revisions_enabled:
             layout = self.client.cache.package_layout(self.ref)
diff --git a/conans/test/functional/configuration/client_certs_test.py b/conans/test/functional/configuration/client_certs_test.py
index 215f54d21..97777f858 100644
--- a/conans/test/functional/configuration/client_certs_test.py
+++ b/conans/test/functional/configuration/client_certs_test.py
@@ -29,7 +29,7 @@ class ClientCertsTest(unittest.TestCase):
         tools.save(client.cache.client_cert_key_path, "Fake key")
         client.init_dynamic_vars()
         self.assertEqual(client.requester.get("url"), (client.cache.client_cert_path,
-                                                        client.cache.client_cert_key_path))
+                                                       client.cache.client_cert_key_path))
 
         # assert that the cacert file is created
         self.assertTrue(os.path.exists(client.cache.cacert_path))
diff --git a/conans/test/functional/corrupted_packages_test.py b/conans/test/functional/corrupted_packages_test.py
new file mode 100644
index 000000000..04a9611cd
--- /dev/null
+++ b/conans/test/functional/corrupted_packages_test.py
@@ -0,0 +1,146 @@
+import os
+import textwrap
+import unittest
+
+from conans.model.ref import ConanFileReference, PackageReference
+from conans.util.env_reader import get_env
+from conans.test.utils.tools import TestClient, TestServer, NO_SETTINGS_PACKAGE_ID
+
+
+class CorruptedPackagesTest(unittest.TestCase):
+    """
+    Simulate a connection failure or file corruption in the server with missing files for a
+    package and make sure the search, install are possible. Check re-upload is always possible
+    even if the package in the server is not accessible
+    """
+
+    def setUp(self):
+        revisions_enabled = get_env("TESTING_REVISIONS_ENABLED", False)
+        self.server = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")])
+        self.client = TestClient(servers={"default": self.server})
+        conanfile = textwrap.dedent("""
+        from conans import ConanFile
+
+        class Pkg(ConanFile):
+            pass
+        """)
+        self.client.save({"conanfile.py": conanfile})
+        self.client.run("create . Pkg/0.1@user/testing")
+        self.client.run("upload * --all --confirm -r default")
+        # Check files are uploded in this order: conan_package.tgz, conaninfo.txt, conanmanifest.txt
+        order1 = str(self.client.out).find("Uploading conan_package.tgz")
+        order2 = str(self.client.out).find("Uploading conaninfo.txt", order1)
+        order3 = str(self.client.out).find("Uploading conanmanifest.txt", order2)
+        self.assertTrue(order1 < order2 < order3)
+        rrev = "210a4a16419aae28fea1f268a8e4f3d4" if revisions_enabled else "0"
+        pref_str = "Pkg/0.1@user/testing#%s" % rrev
+        prev = "11de80325e8db78617b05384825c6409" if revisions_enabled else "0"
+        self.pref = pref = PackageReference(ConanFileReference.loads(pref_str),
+                                            NO_SETTINGS_PACKAGE_ID, prev)
+        self.manifest_path = self.server.server_store.get_package_file_path(pref,
+                                                                            "conanmanifest.txt")
+        self.info_path = self.server.server_store.get_package_file_path(pref, "conaninfo.txt")
+        self.tgz_path = self.server.server_store.get_package_file_path(pref, "conan_package.tgz")
+
+    def _assert_all_package_files_in_server(self):
+        self.assertTrue(os.path.exists(self.manifest_path))
+        self.assertTrue(os.path.exists(self.info_path))
+        self.assertTrue(os.path.exists(self.tgz_path))
+
+    def info_manifest_missing_test(self):
+        os.unlink(self.info_path)
+        os.unlink(self.manifest_path)
+        # Try search
+        self.client.run("search Pkg/0.1@user/testing -r default")
+        self.assertIn("There are no packages for reference 'Pkg/0.1@user/testing', "
+                      "but package recipe found", self.client.out)
+        # Try fresh install
+        self.client.run("remove * -f")
+        self.client.run("install Pkg/0.1@user/testing", assert_error=True)
+        self.assertIn("Pkg/0.1@user/testing:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Missing",
+                      self.client.out)
+        # Try upload of fresh package
+        self.client.run("create . Pkg/0.1@user/testing")
+        self.client.run("upload * --all --confirm -r default")
+        self._assert_all_package_files_in_server()
+
+    def manifest_missing_test(self):
+        os.unlink(self.manifest_path)
+        # Try search
+        self.client.run("search Pkg/0.1@user/testing -r default")
+        self.assertIn("Package_ID: 5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9", self.client.out)
+        # Try fresh install
+        self.client.run("remove * -f")
+        self.client.run("install Pkg/0.1@user/testing", assert_error=True)
+        self.assertIn("ERROR: Binary package not found", self.client.out)
+        self.assertIn(NO_SETTINGS_PACKAGE_ID, self.client.out)
+        # Try upload of fresh package
+        self.client.run("create . Pkg/0.1@user/testing")
+        self.client.run("upload * --all --confirm")
+        self._assert_all_package_files_in_server()
+
+    def tgz_info_missing_test(self):
+        os.unlink(self.tgz_path)
+        os.unlink(self.info_path)
+        # Try search
+        self.client.run("search Pkg/0.1@user/testing -r default")
+        self.assertIn("There are no packages for reference 'Pkg/0.1@user/testing', "
+                      "but package recipe found", self.client.out)
+        # Try fresh install
+        self.client.run("remove * -f")
+        self.client.run("install Pkg/0.1@user/testing", assert_error=True)
+        self.assertIn("Pkg/0.1@user/testing:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Missing",
+                      self.client.out)
+        # Try upload of fresh package
+        self.client.run("create . Pkg/0.1@user/testing")
+        self.client.run("upload * --all --confirm")
+        self.assertIn("Uploading conan_package.tgz", self.client.out)
+        self.assertIn("Uploading conaninfo.txt", self.client.out)
+        self._assert_all_package_files_in_server()
+
+    def tgz_missing_test(self):
+        os.unlink(self.tgz_path)
+        # Try search
+        self.client.run("search Pkg/0.1@user/testing -r default")
+        # Try fresh install
+        self.client.run("remove * -f")
+        self.client.run("install Pkg/0.1@user/testing", assert_error=True)
+        self.assertIn("ERROR: Binary package not found", self.client.out)
+        # Try upload of fresh package
+        self.client.run("create . Pkg/0.1@user/testing")
+        self.client.run("upload * --all --confirm")
+        self.assertIn("Uploading conan_package.tgz", self.client.out)
+        self._assert_all_package_files_in_server()
+
+    def tgz_manifest_missing_test(self):
+        os.unlink(self.tgz_path)
+        os.unlink(self.manifest_path)
+        # Try search
+        self.client.run("search Pkg/0.1@user/testing -r default")
+        self.assertIn("Package_ID: 5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9", self.client.out)
+        # Try fresh install
+        self.client.run("remove * -f")
+        self.client.run("install Pkg/0.1@user/testing", assert_error=True)
+        self.assertIn("ERROR: Binary package not found", self.client.out)
+        # Try upload of fresh package
+        self.client.run("create . Pkg/0.1@user/testing")
+        self.client.run("upload * --all --confirm")
+        self._assert_all_package_files_in_server()
+
+    def tgz_manifest_info_missing_test(self):
+        os.unlink(self.tgz_path)
+        os.unlink(self.manifest_path)
+        os.unlink(self.info_path)
+        # Try search
+        self.client.run("search Pkg/0.1@user/testing -r default")
+        self.assertIn("There are no packages for reference 'Pkg/0.1@user/testing', "
+                      "but package recipe found", self.client.out)
+        # Try fresh install
+        self.client.run("remove * -f")
+        self.client.run("install Pkg/0.1@user/testing", assert_error=True)
+        self.assertIn("Pkg/0.1@user/testing:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Missing",
+                      self.client.out)
+        # Try upload of fresh package
+        self.client.run("create . Pkg/0.1@user/testing")
+        self.client.run("upload * --all --confirm")
+        self._assert_all_package_files_in_server()
diff --git a/conans/test/functional/generators/json_test.py b/conans/test/functional/generators/json_test.py
index 4601752b1..d10ef8c95 100644
--- a/conans/test/functional/generators/json_test.py
+++ b/conans/test/functional/generators/json_test.py
@@ -1,8 +1,9 @@
 import json
 import os
+import textwrap
 import unittest
 
-from conans.test.utils.tools import TestClient
+from conans.test.utils.tools import TestClient, load
 
 
 class JsonTest(unittest.TestCase):
@@ -23,11 +24,12 @@ class HelloConan(ConanFile):
                      "header.h": ""})
         client.run("create . Hello/0.1@lasote/testing")
         client.run("install Hello/0.1@lasote/testing -g json")
-        conan_json = os.path.join(client.current_folder, "conanbuildinfo.json")
-        with open(conan_json) as f:
-            data = json.load(f)
+        conan_json = load(os.path.join(client.current_folder, "conanbuildinfo.json"))
+        data = json.loads(conan_json)
+
         self.assertEqual(data["deps_env_info"]["MY_ENV_VAR"], "foo")
         self.assertEqual(data["deps_user_info"]["Hello"]["my_var"], "my_value")
+
         hello_data = data["dependencies"][0]
         self.assertTrue(os.path.exists(hello_data["rootpath"]))
         include_path = hello_data["include_paths"][0]
@@ -52,9 +54,66 @@ class HelloConan(ConanFile):
         settings = "-sos=Linux -sarch=x86_64"
         client.run("create . Hello/0.1@lasote/testing " + settings)
         client.run("install Hello/0.1@lasote/testing -g json " + settings)
-        conan_json = os.path.join(client.current_folder, "conanbuildinfo.json")
-        with open(conan_json) as f:
-            data = json.load(f)
+
+        conan_json = load(os.path.join(client.current_folder, "conanbuildinfo.json"))
+        data = json.loads(conan_json)
         settings_data = data["settings"]
+
         self.assertEqual(settings_data["os"], "Linux")
         self.assertEqual(settings_data["arch"], "x86_64")
+
+    def test_multiconfig(self):
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+
+            class Lib(ConanFile):
+                settings = "os", "arch"
+                generators = "json"
+                
+                def package_info(self):
+                    self.env_info.MY_ENV_VAR = "foo"
+                    self.user_info.my_var = "my_value"
+                    
+                    self.cpp_info.debug.defines = ["LIB_DEBUG"]
+                    self.cpp_info.release.defines = ["LIB_RELEASE"]
+                    
+                    self.cpp_info.debug.libs = ["Hello_d"]
+                    self.cpp_info.release.libs = ["Hello"]
+            """)
+        client = TestClient()
+        client.save({'conanfile.py': conanfile})
+
+        client.run("create . Hello/0.1@lasote/testing")
+        client.run("install Hello/0.1@lasote/testing -g json")
+
+        my_json = load(os.path.join(client.current_folder, "conanbuildinfo.json"))
+        my_json = json.loads(my_json)
+
+        # Nodes with cpp_info
+        deps_info = my_json["dependencies"][0]
+        deps_info_debug = deps_info["configs"]["debug"]
+        deps_info_release = deps_info["configs"]["release"]
+
+        # Each node should have its own information
+        self.assertListEqual(deps_info["defines"], [])
+        self.assertEqual(deps_info_debug["defines"], ["LIB_DEBUG"])
+        self.assertEqual(deps_info_release["defines"], ["LIB_RELEASE"])
+
+        self.assertListEqual(deps_info["libs"], [])
+        self.assertEqual(deps_info_debug["libs"], ["Hello_d"])
+        self.assertEqual(deps_info_release["libs"], ["Hello"])
+
+        # FIXME: There are _null_ nodes
+        self.assertEqual(deps_info_debug["version"], None)
+        self.assertEqual(deps_info_release["version"], None)
+
+        self.assertEqual(deps_info_debug["description"], None)
+        self.assertEqual(deps_info_release["description"], None)
+
+        # FIXME: Empty (and rootpath) information is duplicated in all the nodes
+        dupe_nodes = ["rootpath", "sysroot", "include_paths", "lib_paths", "bin_paths",
+                      "build_paths", "res_paths", "cflags", "cppflags", "sharedlinkflags",
+                      "exelinkflags"]
+        for dupe in dupe_nodes:
+            self.assertEqual(deps_info[dupe], deps_info_debug[dupe])
+            self.assertEqual(deps_info[dupe], deps_info_release[dupe])
diff --git a/conans/test/integration/install_update_test.py b/conans/test/integration/install_update_test.py
index 47029838c..a9ce81194 100644
--- a/conans/test/integration/install_update_test.py
+++ b/conans/test/integration/install_update_test.py
@@ -234,7 +234,7 @@ class Pkg(ConanFile):
         self.client.run("install Hello0/1.0@lasote/stable --build")
         self.client.run("upload Hello0/1.0@lasote/stable --all")
 
-        client2 = TestClient(servers=self.servers, users={"default": [("lasote", "mypass")]})
+        client2 = TestClient(servers=self.servers, users={"myremote": [("lasote", "mypass")]})
         client2.run("install Hello0/1.0@lasote/stable")
 
         self.assertEqual(str(client2.out).count("Downloading conaninfo.txt"), 1)
diff --git a/conans/test/unittests/client/build/compiler_flags_test.py b/conans/test/unittests/client/build/compiler_flags_test.py
index 039f4ffe4..7c15fcbe3 100644
--- a/conans/test/unittests/client/build/compiler_flags_test.py
+++ b/conans/test/unittests/client/build/compiler_flags_test.py
@@ -32,12 +32,21 @@ class CompilerFlagsTest(unittest.TestCase):
             arch_flag = architecture_flag(arch='armv7', compiler=compiler)
             self.assertEqual(arch_flag, '')
 
+        for compiler in ("gcc", "clang", "sun-cc"):
+            arch_flag = architecture_flag(arch='s390', compiler=compiler)
+            self.assertEqual(arch_flag, '-m31')
+
+        for compiler in ("gcc", "clang", "sun-cc"):
+            arch_flag = architecture_flag(arch='s390x', compiler=compiler)
+            self.assertEqual(arch_flag, '-m64')
+
         arch_flag = architecture_flag(arch='x86', compiler='Visual Studio')
         self.assertEqual(arch_flag, '')
 
         arch_flag = architecture_flag(arch='x86_64', compiler='Visual Studio')
         self.assertEqual(arch_flag, '')
 
+
     def test_libcxx_flags(self):
         arch_define = libcxx_define(compiler='gcc', libcxx='libstdc++')
         self.assertEqual(arch_define, '_GLIBCXX_USE_CXX11_ABI=0')
diff --git a/conans/test/unittests/client/cmd/export/test_update_revision_in_metadata.py b/conans/test/unittests/client/cmd/export/test_update_revision_in_metadata.py
index 8ebfeee70..9e01e97fc 100644
--- a/conans/test/unittests/client/cmd/export/test_update_revision_in_metadata.py
+++ b/conans/test/unittests/client/cmd/export/test_update_revision_in_metadata.py
@@ -2,26 +2,64 @@
 
 
 import unittest
+from collections import namedtuple
 
+import six
 from mock import mock
 
 from conans.client.cmd.export import _update_revision_in_metadata
 from conans.model.ref import ConanFileReference
 from conans.paths.package_layouts.package_cache_layout import PackageCacheLayout
 from conans.test.utils.test_files import temp_folder
+from conans.errors import ConanException
 from conans.test.utils.tools import TestBufferConanOutput
 
 
 class UpdateRevisionInMetadataTests(unittest.TestCase):
 
-    def test_warn_not_pristine(self):
-        output = TestBufferConanOutput()
+    def setUp(self):
+        ref = ConanFileReference.loads("lib/version@user/channel")
+        self.package_layout = PackageCacheLayout(base_folder=temp_folder(), ref=ref,
+                                                 short_paths=False, no_lock=True)
+        self.output = TestBufferConanOutput()
 
+    def test_scm_warn_not_pristine(self):
         with mock.patch("conans.client.cmd.export._detect_scm_revision",
                         return_value=("revision", "git", False)):
-            path = digest = None
-            ref = ConanFileReference.loads("lib/version@user/channel")
-            package_layout = PackageCacheLayout(base_folder=temp_folder(), ref=ref,
-                                                short_paths=False, no_lock=True)
-            _update_revision_in_metadata(package_layout, True, output, path, digest)
-            self.assertIn("WARN: Repo status is not pristine: there might be modified files", output)
+            path = None
+            digest = namedtuple("Digest", "summary_hash")
+            _update_revision_in_metadata(self.package_layout, True, self.output,
+                                         path, digest, "scm")
+            self.assertIn("WARN: Repo status is not pristine: there might be modified files",
+                          self.output)
+
+    def test_scm_behavior(self):
+        revision_mode = "scm"
+
+        digest = None
+        path = None
+        with mock.patch("conans.client.cmd.export._detect_scm_revision",
+                        return_value=("1234", "git", True)):
+            rev = _update_revision_in_metadata(self.package_layout, True, self.output,
+                                               path, digest, revision_mode)
+        self.assertEqual(rev, "1234")
+        self.assertIn("Using git commit as the recipe revision", self.output)
+
+    def test_hash_behavior(self):
+        revision_mode = "hash"
+
+        digest = namedtuple("Digest", "summary_hash")
+        digest.summary_hash = "1234"
+        path = None
+        rev = _update_revision_in_metadata(self.package_layout, True, self.output,
+                                           path, digest, revision_mode)
+        self.assertEqual(rev, "1234")
+        self.assertIn("Using the exported files summary hash as the recipe revision", self.output)
+
+    def test_invalid_behavior(self):
+        revision_mode = "auto"
+        digest = path = None
+
+        with six.assertRaisesRegex(self, ConanException, "Revision mode should be"):
+            _update_revision_in_metadata(self.package_layout, True, self.output,
+                                           path, digest, revision_mode)
diff --git a/conans/test/unittests/util/detected_architecture_test.py b/conans/test/unittests/util/detected_architecture_test.py
index 3605810bb..317ca0766 100644
--- a/conans/test/unittests/util/detected_architecture_test.py
+++ b/conans/test/unittests/util/detected_architecture_test.py
@@ -3,63 +3,36 @@
 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
 
 
+import mock
 import unittest
 
-from mock import mock
+from parameterized import parameterized
 
 from conans.client.tools.oss import detected_architecture
 
 
 class DetectedArchitectureTest(unittest.TestCase):
 
-    def test_various(self):
-
-        # x86
-        with mock.patch("platform.machine", mock.MagicMock(return_value='i386')):
-            self.assertEqual('x86', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='i686')):
-            self.assertEqual('x86', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='x86_64')):
-            self.assertEqual('x86_64', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='amd64')):
-            self.assertEqual('x86_64', detected_architecture())
-
-        # ARM
-        with mock.patch("platform.machine", mock.MagicMock(return_value='aarch64_be')):
-            self.assertEqual('armv8', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='armv8b')):
-            self.assertEqual('armv8', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='armv7l')):
-            self.assertEqual('armv7', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='armv6l')):
-            self.assertEqual('armv6', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='arm')):
-            self.assertEqual('armv6', detected_architecture())
-
-        # PowerPC
-        with mock.patch("platform.machine", mock.MagicMock(return_value='ppc64le')):
-            self.assertEqual('ppc64le', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='ppc64')):
-            self.assertEqual('ppc64', detected_architecture())
-
-        # MIPS
-        with mock.patch("platform.machine", mock.MagicMock(return_value='mips')):
-            self.assertEqual('mips', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='mips64')):
-            self.assertEqual('mips64', detected_architecture())
-
-        # SPARC
-        with mock.patch("platform.machine", mock.MagicMock(return_value='sparc')):
-            self.assertEqual('sparc', detected_architecture())
-
-        with mock.patch("platform.machine", mock.MagicMock(return_value='sparc64')):
-            self.assertEqual('sparcv9', detected_architecture())
+    @parameterized.expand([
+        ['i386', 'x86'],
+        ['i686', 'x86'],
+        ['x86_64', 'x86_64'],
+        ['amd64', 'x86_64'],
+        ['aarch64_be', 'armv8'],
+        ['armv8b', 'armv8'],
+        ['armv7l', 'armv7'],
+        ['armv6l', 'armv6'],
+        ['arm', 'armv6'],
+        ['ppc64le', 'ppc64le'],
+        ['ppc64', 'ppc64'],
+        ['mips', 'mips'],
+        ['mips64', 'mips64'],
+        ['sparc', 'sparc'],
+        ['sparc64', 'sparcv9'],
+        ['s390', 's390'],
+        ['s390x', 's390x']
+    ])
+    def test_various(self, mocked_machine, expected_arch):
+
+        with mock.patch("platform.machine", mock.MagicMock(return_value=mocked_machine)):
+            self.assertEqual(expected_arch, detected_architecture(), "given '%s' expected '%s'" % (mocked_machine, expected_arch))
diff --git a/conans/test/unittests/util/tools_test.py b/conans/test/unittests/util/tools_test.py
index 13fe9594b..c48f123ba 100644
--- a/conans/test/unittests/util/tools_test.py
+++ b/conans/test/unittests/util/tools_test.py
@@ -15,6 +15,7 @@ import six
 from bottle import request, static_file
 from mock.mock import mock_open, patch
 from nose.plugins.attrib import attr
+from parameterized import parameterized
 from six import StringIO
 from six.moves.urllib.parse import quote
 
@@ -797,11 +798,14 @@ class HelloConan(ConanFile):
 
     @unittest.skipUnless(platform.system() == "Windows", "Requires vswhere")
     def vswhere_path_test(self):
+        """
+        Locate vswhere in PATH or in ProgramFiles
+        """
         # vswhere not found
         with tools.environment_append({"ProgramFiles": None, "ProgramFiles(x86)": None, "PATH": ""}):
-            with self.assertRaisesRegex(ConanException, "Cannot locate vswhere"):
+            with six.assertRaisesRegex(self, ConanException, "Cannot locate vswhere"):
                 vswhere()
-        # vswhere in ProgramFiles
+        # vswhere in ProgramFiles but not in PATH
         program_files = get_env("ProgramFiles(x86)") or get_env("ProgramFiles")
         vswhere_path = None
         if program_files:
@@ -811,7 +815,7 @@ class HelloConan(ConanFile):
                 vswhere_path = expected_path
                 with tools.environment_append({"PATH": ""}):
                     self.assertTrue(vswhere())
-        # vswhere in PATH
+        # vswhere in PATH but not in ProgramFiles
         env = {"ProgramFiles": None, "ProgramFiles(x86)": None}
         if not which("vswhere") and vswhere_path:
                 vswhere_folder = os.path.join(program_files, "Microsoft Visual Studio", "Installer")
@@ -1162,171 +1166,50 @@ ProgramFiles(x86)=C:\Program Files (x86)
                        requester=requests, out=out)
         http_server.stop()
 
-    def get_gnu_triplet_test(self):
-        def get_values(this_os, this_arch, setting_os, setting_arch, compiler=None):
-            build = tools.get_gnu_triplet(this_os, this_arch, compiler)
-            host = tools.get_gnu_triplet(setting_os, setting_arch, compiler)
-            return build, host
-
-        build, host = get_values("Linux", "armv6", "Linux", "armv6")
-        self.assertEqual(build, "arm-linux-gnueabi")
-        self.assertEqual(host, "arm-linux-gnueabi")
-
-        build, host = get_values("Linux", "sparc", "Linux", "sparcv9")
-        self.assertEqual(build, "sparc-linux-gnu")
-        self.assertEqual(host, "sparc64-linux-gnu")
-
-        build, host = get_values("Linux", "mips", "Linux", "mips64")
-        self.assertEqual(build, "mips-linux-gnu")
-        self.assertEqual(host, "mips64-linux-gnu")
-
-        build, host = get_values("Linux", "ppc64le", "Linux", "ppc64")
-        self.assertEqual(build, "powerpc64le-linux-gnu")
-        self.assertEqual(host, "powerpc64-linux-gnu")
-
-        build, host = get_values("Linux", "armv5te", "Linux", "arm_whatever")
-        self.assertEqual(build, "arm-linux-gnueabi")
-        self.assertEqual(host, "arm-linux-gnueabi")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "armv7hf")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-gnueabihf")
-
-        build, host = get_values("Linux", "x86", "Linux", "armv7hf")
-        self.assertEqual(build, "x86-linux-gnu")
-        self.assertEqual(host, "arm-linux-gnueabihf")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "x86")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "x86-linux-gnu")
-
-        build, host = get_values("Linux", "x86_64", "Windows", "x86", compiler="gcc")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "i686-w64-mingw32")
-
-        build, host = get_values("Linux", "x86_64", "Windows", "x86", compiler="Visual Studio")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "i686-windows-msvc")  # Not very common but exists sometimes
-
-        build, host = get_values("Linux", "x86_64", "Linux", "armv7hf")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-gnueabihf")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "armv7")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-gnueabi")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "armv6")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-gnueabi")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "armv8_32")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "aarch64-linux-gnu_ilp32")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "armv5el")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-gnueabi")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "armv5hf")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-gnueabihf")
-
-        build, host = get_values("Linux", "x86_64", "Android", "x86")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "i686-linux-android")
-
-        build, host = get_values("Linux", "x86_64", "Android", "x86_64")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "x86_64-linux-android")
-
-        build, host = get_values("Linux", "x86_64", "Android", "armv7")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-androideabi")
-
-        build, host = get_values("Linux", "x86_64", "Android", "armv7hf")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-androideabi")
-
-        build, host = get_values("Linux", "x86_64", "Android", "armv8")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "aarch64-linux-android")
-
-        build, host = get_values("Linux", "x86_64", "Android", "armv6")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "arm-linux-androideabi")
-
-        build, host = get_values("Linux", "x86_64", "Windows", "x86", compiler="gcc")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "i686-w64-mingw32")
-
-        build, host = get_values("Linux", "x86_64", "Windows", "x86_64", compiler="gcc")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "x86_64-w64-mingw32")
-
-        build, host = get_values("Windows", "x86_64", "Windows", "x86", compiler="gcc")
-        self.assertEqual(build, "x86_64-w64-mingw32")
-        self.assertEqual(host, "i686-w64-mingw32")
-
-        build, host = get_values("Windows", "x86_64", "Linux", "armv7hf", compiler="gcc")
-        self.assertEqual(build, "x86_64-w64-mingw32")
-        self.assertEqual(host, "arm-linux-gnueabihf")
-
-        build, host = get_values("Darwin", "x86_64", "Android", "armv7hf")
-        self.assertEqual(build, "x86_64-apple-darwin")
-        self.assertEqual(host, "arm-linux-androideabi")
-
-        build, host = get_values("Darwin", "x86_64", "Macos", "x86")
-        self.assertEqual(build, "x86_64-apple-darwin")
-        self.assertEqual(host, "i686-apple-darwin")
-
-        build, host = get_values("Darwin", "x86_64", "iOS", "armv7")
-        self.assertEqual(build, "x86_64-apple-darwin")
-        self.assertEqual(host, "arm-apple-darwin")
-
-        build, host = get_values("Darwin", "x86_64", "watchOS", "armv7k")
-        self.assertEqual(build, "x86_64-apple-darwin")
-        self.assertEqual(host, "arm-apple-darwin")
-
-        build, host = get_values("Darwin", "x86_64", "tvOS", "armv8")
-        self.assertEqual(build, "x86_64-apple-darwin")
-        self.assertEqual(host, "aarch64-apple-darwin")
-
-        build, host = get_values("Darwin", "x86_64", "tvOS", "armv8.3")
-        self.assertEqual(build, "x86_64-apple-darwin")
-        self.assertEqual(host, "aarch64-apple-darwin")
-
-        build, host = get_values("Darwin", "x86_64", "watchOS", "armv8_32")
-        self.assertEqual(build, "x86_64-apple-darwin")
-        self.assertEqual(host, "aarch64-apple-darwin")
-
-        build, host = get_values("Linux", "x86_64", "Linux", "ppc32")
-        self.assertEqual(build, "x86_64-linux-gnu")
-        self.assertEqual(host, "powerpc-linux-gnu")
-
-        build, host = get_values("Linux", "x86", "Linux", "ppc64")
-        self.assertEqual(build, "x86-linux-gnu")
-        self.assertEqual(host, "powerpc64-linux-gnu")
-
-        for _os in ["Windows", "Linux"]:
-            for arch in ["x86_64", "x86"]:
-                triplet = tools.get_gnu_triplet(_os, arch, "gcc")
-
-                output = ""
-                if arch == "x86_64":
-                    output += "x86_64"
-                else:
-                    output += "i686" if _os != "Linux" else "x86"
-
-                output += "-"
-                if _os == "Windows":
-                    output += "w64-mingw32"
-                else:
-                    output += "linux-gnu"
-
-                self.assertIn(output, triplet)
-
-        # Compiler not specified for os="Windows"
+    @parameterized.expand([
+        ["Linux", "x86", None, "x86-linux-gnu"],
+        ["Linux", "x86_64", None, "x86_64-linux-gnu"],
+        ["Linux", "armv6", None, "arm-linux-gnueabi"],
+        ["Linux", "sparc", None, "sparc-linux-gnu"],
+        ["Linux", "sparcv9", None, "sparc64-linux-gnu"],
+        ["Linux", "mips", None, "mips-linux-gnu"],
+        ["Linux", "mips64", None, "mips64-linux-gnu"],
+        ["Linux", "ppc32", None, "powerpc-linux-gnu"],
+        ["Linux", "ppc64", None, "powerpc64-linux-gnu"],
+        ["Linux", "ppc64le", None, "powerpc64le-linux-gnu"],
+        ["Linux", "armv5te", None, "arm-linux-gnueabi"],
+        ["Linux", "arm_whatever", None, "arm-linux-gnueabi"],
+        ["Linux", "armv7hf", None, "arm-linux-gnueabihf"],
+        ["Linux", "armv6", None, "arm-linux-gnueabi"],
+        ["Linux", "armv7", None, "arm-linux-gnueabi"],
+        ["Linux", "armv8_32", None, "aarch64-linux-gnu_ilp32"],
+        ["Linux", "armv5el", None, "arm-linux-gnueabi"],
+        ["Linux", "armv5hf", None, "arm-linux-gnueabihf"],
+        ["Linux", "s390", None, "s390-ibm-linux-gnu"],
+        ["Linux", "s390x", None, "s390x-ibm-linux-gnu"],
+        ["Android", "x86", None, "i686-linux-android"],
+        ["Android", "x86_64", None, "x86_64-linux-android"],
+        ["Android", "armv6", None, "arm-linux-androideabi"],
+        ["Android", "armv7", None, "arm-linux-androideabi"],
+        ["Android", "armv7hf", None, "arm-linux-androideabi"],
+        ["Android", "armv8", None, "aarch64-linux-android"],
+        ["Windows", "x86", "Visual Studio", "i686-windows-msvc"],
+        ["Windows", "x86", "gcc", "i686-w64-mingw32"],
+        ["Windows", "x86_64", "gcc", "x86_64-w64-mingw32"],
+        ["Darwin", "x86_64", None, "x86_64-apple-darwin"],
+        ["Macos", "x86", None, "i686-apple-darwin"],
+        ["iOS", "armv7", None, "arm-apple-darwin"],
+        ["watchOS", "armv7k", None, "arm-apple-darwin"],
+        ["watchOS", "armv8_32", None, "aarch64-apple-darwin"],
+        ["tvOS", "armv8", None, "aarch64-apple-darwin"],
+        ["tvOS", "armv8.3", None, "aarch64-apple-darwin"]
+    ])
+    def get_gnu_triplet_test(self, os, arch, compiler, expected_triplet):
+        triplet = tools.get_gnu_triplet(os, arch, compiler)
+        self.assertEqual(triplet, expected_triplet,
+                          "triplet did not match for ('%s', '%s', '%s')" % (os, arch, compiler))
+
+    def get_gnu_triplet_on_windows_without_compiler_test(self):
         with self.assertRaises(ConanException):
             tools.get_gnu_triplet("Windows", "x86")