Repository: conan-io/conan
Pull request #4526 (2019-02-15 07:54:26)

Fix Anonymous user behavior + Artifactory test framework
Changelog: Fix:  Fixed issue with Artifactory when the anonymous user is enabled, causing the uploads to fail without requesting the user and password.
Docs: omit

- new Artifactory test server framework, only used with "revisions_test", other tests might have more "server checks" that would need to be implemented.
- To enable the artifactory server automatically you can use `CONAN_TEST_WITH_ARTIFACTORY=1`
- Fixed issue with Artifactory when the anonymous user is enabled. The "check_credentials" endpoint returns ok but then the upload returns "403" instead of "401" causing Conan to crash without requesting user and password.




Patch:
diff --git a/.ci/jenkins/Jenkinsfile b/.ci/jenkins/Jenkinsfile
index ad1b98269..1c4e1a825 100644
--- a/.ci/jenkins/Jenkinsfile
+++ b/.ci/jenkins/Jenkinsfile
@@ -106,42 +106,44 @@ for (n in ["Windows", "Linux"]) {
     rest_builders["${the_node} Rest API Test"] = {
         node(the_node){
             stage("REST tests ${the_node}"){
-                def vars = checkout scm
-                def base_dir = (the_node == "Windows") ? win_tmp_base : rest_tmp_base
-                base_dir = base_dir + "rest" + vars["GIT_COMMIT"].substring(0, 4)
-                def venv_dir = base_dir + "/venv"
-                def src_dir = base_dir + "/src"
+                lock('restApiTestsLock') {
+                    def vars = checkout scm
+                    def base_dir = (the_node == "Windows") ? win_tmp_base : rest_tmp_base
+                    base_dir = base_dir + "rest" + vars["GIT_COMMIT"].substring(0, 4)
+                    def venv_dir = base_dir + "/venv"
+                    def src_dir = base_dir + "/src"
 
-                while(fileExists(src_dir)){
-                    src_dir = src_dir + "_"
-                }
-                while(fileExists(venv_dir)){
-                    venv_dir = venv_dir + "_"
-                }
-
-                // Copy SRC to a clean folder
-                dir(base_dir){ // Trick to create the parent
-                    def escaped_ws = "${WORKSPACE}".replace("\\", "/")
-                    def cmd = "python -c \"import shutil; shutil.copytree('${escaped_ws}', '${src_dir}')\""
-                    if (the_node == "Windows"){
-                        bat(script: cmd)
+                    while(fileExists(src_dir)){
+                        src_dir = src_dir + "_"
                     }
-                    else{
-                        sh(script: cmd)
+                    while(fileExists(venv_dir)){
+                        venv_dir = venv_dir + "_"
                     }
-                }
 
-                if(the_node == "Windows"){
-                    try{
-                      bat(script: "python ${runner} conans.test.functional.remote.rest_api_test ${pyver} ${src_dir} \"${venv_dir}\"")
+                    // Copy SRC to a clean folder
+                    dir(base_dir){ // Trick to create the parent
+                        def escaped_ws = "${WORKSPACE}".replace("\\", "/")
+                        def cmd = "python -c \"import shutil; shutil.copytree('${escaped_ws}', '${src_dir}')\""
+                        if (the_node == "Windows"){
+                            bat(script: cmd)
+                        }
+                        else{
+                            sh(script: cmd)
+                        }
                     }
-                    finally{
-                      bat(script: "rd /s /q \"${base_dir}\"")
+
+                    if(the_node == "Windows"){
+                        try{
+                          bat(script: "python ${runner} conans.test.functional.remote.rest_api_test ${pyver} ${src_dir} \"${venv_dir}\"")
+                        }
+                        finally{
+                          bat(script: "rd /s /q \"${base_dir}\"")
+                        }
                     }
-                }
-                else{
-                    docker.image('conanio/conantests').inside("-e CONAN_USER_HOME=${src_dir} -v${src_dir}:${src_dir}") {
-                      sh(script: "python ${runner} conans.test.functional.remote.rest_api_test ${pyver} ${src_dir} /tmp")
+                    else{
+                        docker.image('conanio/conantests').inside("-e CONAN_USER_HOME=${src_dir} -v${src_dir}:${src_dir}") {
+                          sh(script: "python ${runner} conans.test.functional.remote.rest_api_test ${pyver} ${src_dir} /tmp")
+                        }
                     }
                 }
             }
diff --git a/conans/client/command.py b/conans/client/command.py
index 579c44347..6a60b7cc1 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -1353,7 +1353,8 @@ class Command(object):
                                  'conanfile if only a reference is specified and a conaninfo.txt '
                                  'file contents if the package is also specified',
                             default=None, nargs="?")
-        parser.add_argument("-p", "--package", default=None, help='Package ID',
+        parser.add_argument("-p", "--package", default=None,
+                            help="Package ID [DEPRECATED: use full reference instead]",
                             action=OnceArgument)
         parser.add_argument("-r", "--remote", action=OnceArgument,
                             help='Get from this specific remote')
@@ -1361,7 +1362,24 @@ class Command(object):
                             help='Do not decorate the text')
         args = parser.parse_args(*args)
 
-        ret, path = self._conan.get_path(args.reference, args.package, args.path, args.remote)
+        try:
+            pref = PackageReference.loads(args.reference, validate=True)
+            reference = pref.ref.full_repr()
+            package_id = pref.id
+        except ConanException:
+            reference = args.reference
+            package_id = args.package
+
+            if package_id:
+                self._user_io.out.warn("Usage of `--package` argument is deprecated."
+                                       " Use a full reference instead: "
+                                       "`conan get [...] {}:{}`".format(reference, package_id))
+        else:
+            if args.package:
+                raise ConanException("Use a full package reference (preferred) or the `--package`"
+                                     " command argument, but not both.")
+
+        ret, path = self._conan.get_path(reference, package_id, args.path, args.remote)
         if isinstance(ret, list):
             self._outputer.print_dir_list(ret, path, args.raw)
         else:
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index 538bdee2e..bd13f228f 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -925,8 +925,8 @@ class ConanAPIV1(object):
             path = "conanfile.py" if not package_id else "conaninfo.txt"
 
         if not remote_name:
-            from conans.client.local_file_getter import get_path
-            return get_path(self._cache, ref, package_id, path), path
+            package_layout = self._cache.package_layout(ref, short_paths=None)
+            return package_layout.get_path(path=path, package_id=package_id), path
         else:
             remote = self.get_remote_by_name(remote_name)
             if self._cache.config.revisions_enabled and not ref.revision:
diff --git a/conans/client/file_copier.py b/conans/client/file_copier.py
index 463aba1c7..c21c69a48 100644
--- a/conans/client/file_copier.py
+++ b/conans/client/file_copier.py
@@ -16,12 +16,12 @@ def report_copied_files(copied, output, message_suffix="Copied"):
         return False
 
     for ext, files in ext_files.items():
-        files_str = (", ".join(files)) if len(files) < 5 else ""
+        files_str = (": " + ", ".join(files)) if len(files) < 5 else ""
         file_or_files = "file" if len(files) == 1 else "files"
         if not ext:
-            output.info("%s %d %s: %s" % (message_suffix, len(files), file_or_files, files_str))
+            output.info("%s %d %s%s" % (message_suffix, len(files), file_or_files, files_str))
         else:
-            output.info("%s %d '%s' %s: %s" % (message_suffix, len(files), ext, file_or_files, files_str))
+            output.info("%s %d '%s' %s%s" % (message_suffix, len(files), ext, file_or_files, files_str))
     return True
 
 
diff --git a/conans/client/local_file_getter.py b/conans/client/local_file_getter.py
deleted file mode 100644
index f91328677..000000000
--- a/conans/client/local_file_getter.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import os
-
-from conans.errors import NotFoundException
-from conans.model.manifest import discarded_file
-from conans.model.ref import PackageReference
-from conans.util.files import load
-
-
-def get_path(cache, ref, package_id, path):
-    """
-    :param cache: Conan's client cache
-    :param ref: Specified reference in the conan get command
-    :param package_id: Specified package id (can be None)
-    :param path: Path to a file, subfolder of exports (if only ref)
-                 or package (if package_id defined)
-    :return: The real path in the local cache for the specified parameters
-    """
-    if package_id is None:  # Get the file in the exported files
-        folder = cache.export(ref)
-    else:
-        folder = cache.package(PackageReference(ref, package_id), short_paths=None)
-
-    abs_path = os.path.join(folder, path)
-    if not os.path.exists(abs_path):
-        raise NotFoundException("The specified path doesn't exist")
-    if os.path.isdir(abs_path):
-        return sorted([path for path in os.listdir(abs_path) if not discarded_file(path)])
-    else:
-        return load(abs_path)
diff --git a/conans/client/rest/rest_client_v2.py b/conans/client/rest/rest_client_v2.py
index 9c150b84c..5362fbcbd 100644
--- a/conans/client/rest/rest_client_v2.py
+++ b/conans/client/rest/rest_client_v2.py
@@ -5,11 +5,10 @@ import time
 
 from conans.client.remote_manager import check_compressed_files
 from conans.client.rest.client_routes import ClientV2Router
-from conans.client.rest.rest_client_common import RestCommonMethods, get_exception_from_error, \
-    handle_return_deserializer
+from conans.client.rest.rest_client_common import RestCommonMethods, get_exception_from_error
 from conans.client.rest.uploader_downloader import Downloader, Uploader
 from conans.errors import ConanException, NotFoundException, PackageNotFoundException, \
-    RecipeNotFoundException
+    RecipeNotFoundException, AuthenticationException, ForbiddenException
 from conans.model.info import ConanInfo
 from conans.model.manifest import FileTreeManifest
 from conans.model.ref import PackageReference
@@ -192,6 +191,8 @@ class RestV2Methods(RestCommonMethods):
                     failed.append(filename)
                 else:
                     pass
+            except (AuthenticationException, ForbiddenException):
+                raise
             except Exception as exc:
                 self._output.error("\nError uploading file: %s, '%s'" % (filename, exc))
                 failed.append(filename)
diff --git a/conans/client/rest/uploader_downloader.py b/conans/client/rest/uploader_downloader.py
index e8b547edb..7908e3025 100644
--- a/conans/client/rest/uploader_downloader.py
+++ b/conans/client/rest/uploader_downloader.py
@@ -28,6 +28,10 @@ class Uploader(object):
                 dedup_headers.update(headers)
             response = self.requester.put(url, data="", verify=self.verify, headers=dedup_headers,
                                           auth=auth)
+            if response.status_code == 403:
+                if auth.token is None:
+                    raise AuthenticationException(response.content)
+                raise ForbiddenException(response.content)
             if response.status_code == 201:  # Artifactory returns 201 if the file is there
                 return response
 
@@ -49,6 +53,12 @@ class Uploader(object):
         try:
             response = self.requester.put(url, data=data, verify=self.verify,
                                           headers=headers, auth=auth)
+            if response.status_code == 403:
+                if auth.token is None:
+                    raise AuthenticationException(response.content)
+                raise ForbiddenException(response.content)
+        except ConanException:
+            raise
         except Exception as exc:
             raise ConanException(exc)
 
diff --git a/conans/errors.py b/conans/errors.py
index b4677b1ce..8333b58b2 100644
--- a/conans/errors.py
+++ b/conans/errors.py
@@ -187,7 +187,7 @@ class PackageNotFoundException(NotFoundException):
 
     def __str__(self):
         tmp = self.pref.full_repr() if self.print_rev else str(self.pref)
-        return "Binary package not found: '{}'%s".format(tmp, self.remote_message())
+        return "Binary package not found: '{}'{}".format(tmp, self.remote_message())
 
 
 class UserInterfaceErrorException(RequestErrorException):
diff --git a/conans/model/ref.py b/conans/model/ref.py
index c986237a6..cdb58e074 100644
--- a/conans/model/ref.py
+++ b/conans/model/ref.py
@@ -154,7 +154,7 @@ class PackageReference(namedtuple("PackageReference", "ref id revision")):
         text = text.strip()
         tmp = text.split(":")
         try:
-            ref = ConanFileReference.loads(tmp[0].strip())
+            ref = ConanFileReference.loads(tmp[0].strip(), validate=validate)
             package_id = tmp[1].strip()
         except IndexError:
             raise ConanException("Wrong package reference  %s" % text)
diff --git a/conans/paths/package_layouts/package_cache_layout.py b/conans/paths/package_layouts/package_cache_layout.py
index 3552c5760..b74a01061 100644
--- a/conans/paths/package_layouts/package_cache_layout.py
+++ b/conans/paths/package_layouts/package_cache_layout.py
@@ -4,8 +4,10 @@ import os
 import platform
 from contextlib import contextmanager
 
+from conans.errors import NotFoundException
 from conans.errors import RecipeNotFoundException, PackageNotFoundException
 from conans.model.manifest import FileTreeManifest
+from conans.model.manifest import discarded_file
 from conans.model.package_metadata import PackageMetadata
 from conans.model.ref import ConanFileReference
 from conans.model.ref import PackageReference
@@ -144,3 +146,28 @@ class PackageCacheLayout(object):
         except IOError:
             raise PackageNotFoundException(pref)
         return read_manifest.summary_hash
+
+    # Raw access to file
+    def get_path(self, path, package_id=None):
+        """ Return the contents for the given `path` inside current layout, it can
+            be a single file or the list of files in a directory
+
+            :param package_id: will retrieve the contents from the package directory
+            :param path: path relative to the cache reference or package folder
+        """
+
+        assert not os.path.isabs(path)
+
+        if package_id is None:  # Get the file in the exported files
+            folder = self.export()
+        else:
+            pref = PackageReference(self._ref, package_id)
+            folder = self.package(pref)
+
+        abs_path = os.path.join(folder, path)
+        if not os.path.exists(abs_path):
+            raise NotFoundException("The specified path doesn't exist")
+        if os.path.isdir(abs_path):
+            return sorted([path for path in os.listdir(abs_path) if not discarded_file(path)])
+        else:
+            return load(abs_path)
diff --git a/conans/paths/package_layouts/package_editable_layout.py b/conans/paths/package_layouts/package_editable_layout.py
index 066043e6c..3ca70b1d9 100644
--- a/conans/paths/package_layouts/package_editable_layout.py
+++ b/conans/paths/package_layouts/package_editable_layout.py
@@ -53,3 +53,6 @@ class PackageEditableLayout(object):
 
     def package_metadata(self):
         raise ConanException("Package metadata is not available for editable packages")
+
+    def get_path(self, package_id=None, path=None):
+        raise ConanException("Operation not allowed on a package installed as editable")


Test patch:
diff --git a/conans/test/functional/command/conan_get_test.py b/conans/test/functional/command/conan_get_test.py
index 81c8925af..8c5ef688d 100644
--- a/conans/test/functional/command/conan_get_test.py
+++ b/conans/test/functional/command/conan_get_test.py
@@ -1,6 +1,8 @@
 import unittest
 
 import mock
+import textwrap
+from parameterized import parameterized
 
 from conans.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer
 
@@ -8,50 +10,51 @@ from conans.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServ
 class ConanGetTest(unittest.TestCase):
 
     def setUp(self):
-        self.conanfile = '''
-from conans import ConanFile
-
-class HelloConan(ConanFile):
-    name = "Hello0"
-    version = "0.1"
-    exports_sources = "path*"
-    exports = "other*"
-        '''
-
-        test_server = TestServer([],  # write permissions
-                                 users={"lasote": "mypass"})  # exported users and passwords
+        self.reference = "Hello0/0.1@lasote/channel"
+        self.conanfile = textwrap.dedent('''
+            from conans import ConanFile
+            
+            class HelloConan(ConanFile):
+                name = "Hello0"
+                version = "0.1"
+                exports_sources = "path*"
+                exports = "other*"
+            ''')
+
+        test_server = TestServer([], users={"lasote": "mypass"})
         servers = {"default": test_server}
         self.client = TestClient(servers=servers, users={"default": [("lasote", "mypass")]})
-        files = {"conanfile.py": self.conanfile, "path/to/exported_source": "1",
+        files = {"conanfile.py": self.conanfile,
+                 "path/to/exported_source": "1",
                  "other/path/to/exported": "2"}
         self.client.save(files)
         self.client.run("export . lasote/channel")
-        self.client.run("install Hello0/0.1@lasote/channel --build missing")
+        self.client.run("install {} --build missing".format(self.reference))
 
-    def test_get_local(self):
+    def test_get_local_reference(self):
         # Local search, dir list
-        self.client.run('get Hello0/0.1@lasote/channel .')
+        self.client.run('get {} .'.format(self.reference))
         self.assertEquals("""Listing directory '.':
  conanfile.py
  conanmanifest.txt
  other
 """, self.client.user_io.out)
 
-        self.client.run('get Hello0/0.1@lasote/channel other --raw')
+        self.client.run('get {} other --raw'.format(self.reference))
         self.assertEquals("path\n", self.client.user_io.out)
 
-        self.client.run('get Hello0/0.1@lasote/channel other/path --raw')
+        self.client.run('get {} other/path --raw'.format(self.reference))
         self.assertEquals("to\n", self.client.user_io.out)
 
-        self.client.run('get Hello0/0.1@lasote/channel other/path/to')
+        self.client.run('get {} other/path/to'.format(self.reference))
         self.assertEquals("Listing directory 'other/path/to':\n exported\n",
                           self.client.user_io.out)
 
-        self.client.run('get Hello0/0.1@lasote/channel other/path/to/exported')
+        self.client.run('get {} other/path/to/exported'.format(self.reference))
         self.assertEquals("2\n", self.client.user_io.out)
 
         # Local search, conanfile print
-        self.client.run('get Hello0/0.1@lasote/channel --raw')
+        self.client.run('get {} --raw'.format(self.reference))
         self.assertIn(self.conanfile, self.client.user_io.out)
 
         # Get conanfile, verify the formatter applied
@@ -63,55 +66,101 @@ class HelloConan(ConanFile):
 
         with mock.patch("pygments.highlight", verify_python_lexer):
             with mock.patch("conans.client.output.ConanOutput.is_terminal", True):
-                self.client.run('get Hello0/0.1@lasote/channel')
+                self.client.run('get {}'.format(self.reference))
         self.assertIn("Hi there! I'm a mock!", self.client.out)
 
-        # Local search print package info
-        self.client.run('get Hello0/0.1@lasote/channel -p %s --raw' % NO_SETTINGS_PACKAGE_ID)
-        self.assertIn("""
-[requires]
-
-
-[options]
-
-
-[full_settings]
-
-
-[full_requires]
-
-
-[full_options]
-
-
-[recipe_hash]
-    dcb3ad84f4e46fb3da40dbbea0017094
+    @parameterized.expand([(True,), (False,)])
+    def test_get_local_package_reference(self, use_pkg_reference):
+        args_reference = self.reference if not use_pkg_reference else \
+            "{}:{}".format(self.reference, NO_SETTINGS_PACKAGE_ID)
+        args_package = " -p {}".format(NO_SETTINGS_PACKAGE_ID) if not use_pkg_reference else ""
 
-""", self.client.user_io.out)
+        # Local search print package info
+        self.client.run('get {} {} --raw'.format(args_reference, args_package))
+        self.assertIn(textwrap.dedent("""
+            [requires]
+            
+            
+            [options]
+            
+            
+            [full_settings]
+            
+            
+            [full_requires]
+            
+            
+            [full_options]
+            
+            
+            [recipe_hash]
+                07e4bf611af374672215a94d84146e2d
+            
+            """), self.client.user_io.out)
 
         # List package dir
-        self.client.run('get Hello0/0.1@lasote/channel "." -p %s --raw' % NO_SETTINGS_PACKAGE_ID)
-        self.assertEquals("conaninfo.txt\nconanmanifest.txt\n", self.client.user_io.out)
+        self.client.run('get {} "." {} --raw'.format(args_reference, args_package))
+        assert_cmp = self.assertEqual if use_pkg_reference else self.assertIn
+        assert_cmp("conaninfo.txt\nconanmanifest.txt\n", self.client.user_io.out)
 
-    def test_get_remote(self):
+    def test_get_remote_reference(self):
         self.client.run('upload "Hello*" --all -c')
 
         # Remote search, dir list
-        self.client.run('get Hello0/0.1@lasote/channel . -r default --raw')
+        self.client.run('get {} . -r default --raw'.format(self.reference))
         self.assertIn("conan_export.tgz\nconan_sources.tgz\nconanfile.py\nconanmanifest.txt",
                       self.client.user_io.out)
 
         # Remote search, conanfile print
-        self.client.run('get Hello0/0.1@lasote/channel -r default --raw')
+        self.client.run('get {} -r default --raw'.format(self.reference))
         self.assertIn(self.conanfile, self.client.user_io.out)
 
+    @parameterized.expand([(True,), (False,)])
+    def test_get_remote_package_reference(self, use_pkg_reference):
+        args_reference = self.reference if not use_pkg_reference else \
+            "{}:{}".format(self.reference, NO_SETTINGS_PACKAGE_ID)
+        args_package = " -p {}".format(NO_SETTINGS_PACKAGE_ID) if not use_pkg_reference else ""
+
+        self.client.run('upload "Hello*" --all -c')
+
         # List package dir
-        self.client.run('get Hello0/0.1@lasote/channel "." '
-                        '  -p %s --raw -r default' % NO_SETTINGS_PACKAGE_ID)
-        self.assertEquals("conan_package.tgz\nconaninfo.txt\nconanmanifest.txt\n",
-                          self.client.user_io.out)
+        self.client.run('get {} "." {} --raw -r default'.format(args_reference, args_package))
+        assert_cmp = self.assertEqual if use_pkg_reference else self.assertIn
+        assert_cmp("conan_package.tgz\nconaninfo.txt\nconanmanifest.txt\n", self.client.user_io.out)
+
+    def test_not_found_reference(self):
+        self.client.run('get {} "." -r default'.format(self.reference), assert_error=True)
+        self.assertIn("Recipe not found: '{}'".format(self.reference), self.client.user_io.out)
+
+    @parameterized.expand([(True,), (False,)])
+    def test_not_found_package_reference(self, use_pkg_reference):
+        fake_package_id = "123123123123123"
+        args_reference = self.reference if not use_pkg_reference else \
+            "{}:{}".format(self.reference, fake_package_id)
+        args_package = " -p {}".format(fake_package_id) if not use_pkg_reference else ""
+
+        self.client.run('get {} "." -r default {}'.format(args_reference, args_package),
+                        assert_error=True)
+        self.assertIn("ERROR: Binary package not found: '{}:{}'".format(self.reference,
+                                                                        fake_package_id),
+                      self.client.user_io.out)
+
+    def test_duplicated_input(self):
+        """ Fail if given the full reference and the `-p` argument (even if they are equal)"""
+        self.client.run('get {reference}:{pkg_id} -p {pkg_id}'.format(reference=self.reference,
+                                                                      pkg_id=NO_SETTINGS_PACKAGE_ID),
+                        assert_error=True)
+        self.assertIn("Use a full package reference (preferred) or the `--package` "
+                      "command argument, but not both.", self.client.out)
+
+    def test_deprecated_argument(self):
+        """ Argument `-p` should print a deprecation warning (and work)"""
+        self.client.run('get {reference} -p {pkg_id}'.format(reference=self.reference,
+                                                             pkg_id=NO_SETTINGS_PACKAGE_ID))
+        self.assertIn("WARN: Usage of `--package` argument is deprecated. Use a full reference "
+                      "instead: `conan get [...] ", self.client.out)
 
-    def test_not_found(self):
+    """
         self.client.run('get Hello0/0.1@lasote/channel "." -r default', assert_error=True)
         self.assertIn("Recipe not found: 'Hello0/0.1@lasote/channel'", self.client.user_io.out)
 
@@ -123,3 +172,4 @@ class HelloConan(ConanFile):
         else:
             self.assertIn("Binary package not found: 'Hello0/0.1@lasote/channel:123123123123123'",
                           self.client.user_io.out)
+    """
\ No newline at end of file
diff --git a/conans/test/integration/revisions_test.py b/conans/test/integration/revisions_test.py
index 49c803356..c10b6e378 100644
--- a/conans/test/integration/revisions_test.py
+++ b/conans/test/integration/revisions_test.py
@@ -1091,7 +1091,12 @@ class SearchingPackagesWithRevisions(unittest.TestCase):
     def search_in_remote_by_revision_pattern_test(self, v1):
         """If we search for recipes with a pattern like "lib/1.0@conan/stable#rev*"
          1. With v2 client: We get the revs without refs matching the pattern
-         2. With v1 client: Same"""
+         2. With v1 client: Same
+
+         The same for "lib/*@conan/stable#rev" and "*lib/*@conan/stable#rev"
+
+         But if we search an invalid revision it is not found
+         """
 
         # Upload to the server two rrevs for "lib" and one rrevs for "lib2"
         self.c_v2.create(self.ref)
@@ -1111,6 +1116,24 @@ class SearchingPackagesWithRevisions(unittest.TestCase):
         expected = [str(self.ref)]
         self.assertEquals(expected, [i["recipe"]["id"] for i in items])
 
+        data = client.search("{}".format(pref2_lib.ref.full_repr()).replace("1.0", "*"),
+                             remote="default")
+        items = data["results"][0]["items"]
+        expected = [str(self.ref)]
+        self.assertEquals(expected, [i["recipe"]["id"] for i in items])
+
+        data = client.search("*{}".format(pref2_lib.ref.full_repr()).replace("1.0", "*"),
+                             remote="default")
+        items = data["results"][0]["items"]
+        expected = [str(self.ref)]
+        self.assertEquals(expected, [i["recipe"]["id"] for i in items])
+
+        data = client.search("*{}#fakerev".format(pref2_lib.ref),
+                             remote="default")
+        items = data["results"]
+        expected = []
+        self.assertEquals(expected, items)
+
     def search_revisions_locally_with_v1_server_test(self):
         """If I upload a recipe to a v1 server and then I check the revisions locally, it
         will return None to the time because the v1 doesn't support revisions"""
@@ -1348,6 +1371,7 @@ class ServerRevisionsIndexes(unittest.TestCase):
 
     def setUp(self):
         self.server = TestServer()
+        self.c_v1 = TurboTestClient(revisions_enabled=False, servers={"default": self.server})
         self.c_v2 = TurboTestClient(revisions_enabled=True, servers={"default": self.server})
         self.ref = ConanFileReference.loads("lib/1.0@conan/testing")
 
@@ -1482,3 +1506,19 @@ class ServerRevisionsIndexes(unittest.TestCase):
         revs = [r.revision
                 for r in self.server.server_store.get_package_revisions(pref)]
         self.assertEquals(revs, [pref4.revision])
+
+    def v1_get_always_latest_test(self):
+        conanfile = GenConanfile()
+        self.c_v1.create(self.ref, conanfile=conanfile)
+        self.c_v1.upload_all(self.ref)
+        pref = self.c_v2.create(self.ref, conanfile=conanfile.with_build_msg("Rev2"))
+        self.c_v2.upload_all(self.ref)
+
+        latest = self.server.server_store.get_last_revision(self.ref)
+        self.assertEquals(latest.revision, pref.ref.revision)
+
+        self.c_v1.create(self.ref, conanfile=conanfile.with_build_msg("Rev3"))
+        self.c_v1.upload_all(self.ref)
+
+        latest = self.server.server_store.get_last_revision(self.ref)
+        self.assertEquals(latest.revision, DEFAULT_REVISION_V1)
diff --git a/conans/test/unittests/client/file_copier/__init__.py b/conans/test/unittests/client/file_copier/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conans/test/unittests/client/file_copier/test_report_copied_files.py b/conans/test/unittests/client/file_copier/test_report_copied_files.py
new file mode 100644
index 000000000..37737d7e1
--- /dev/null
+++ b/conans/test/unittests/client/file_copier/test_report_copied_files.py
@@ -0,0 +1,29 @@
+# coding=utf-8
+
+import unittest
+
+from conans.client.file_copier import report_copied_files
+from conans.test.utils.tools import TestBufferConanOutput
+
+
+class ReportCopiedFilesTestCase(unittest.TestCase):
+
+    def test_output_string(self):
+        output = TestBufferConanOutput()
+
+        files = ['/abs/path/to/file.pdf',
+                 '../rel/path/to/file2.pdf',
+                 '../rel/path/to/file3.pdf',
+                 '../rel/path/to/file4.pdf',
+                 '../rel/path/to/file5.pdf',
+                 '../rel/path/to/file6.pdf',
+                 '../rel/path/to/file7.pdf',
+                 '/without/ext/no_ext1',
+                 'no_ext2',
+                 'a/other.txt']
+
+        report_copied_files(files, output)
+        lines = sorted(str(output).splitlines())
+        self.assertEqual("Copied 7 '.pdf' files", lines[2])
+        self.assertEqual("Copied 2 files: no_ext1, no_ext2", lines[1])
+        self.assertEqual("Copied 1 '.txt' file: other.txt", lines[0])
diff --git a/conans/test/utils/conanfile.py b/conans/test/utils/conanfile.py
index b9e6adb9f..0be3bb807 100644
--- a/conans/test/utils/conanfile.py
+++ b/conans/test/utils/conanfile.py
@@ -1,7 +1,7 @@
 import os
 from collections import namedtuple
 
-from conans import ConanFile, Options
+from conans import Options
 from conans.model.conan_file import ConanFile
 from conans.model.options import PackageOptions
 from conans.test.utils.tools import TestBufferConanOutput
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
index c8bb0ed01..7318f12e4 100644
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -13,6 +13,7 @@ from contextlib import contextmanager
 from io import StringIO
 
 import bottle
+import nose
 import requests
 import shutil
 import six
@@ -40,11 +41,12 @@ from conans.client.tools.files import replace_in_file
 from conans.client.tools.scm import Git, SVN
 from conans.client.tools.win import get_cased_path
 from conans.client.userio import UserIO
-from conans.errors import NotFoundException
+from conans.errors import NotFoundException, RecipeNotFoundException, PackageNotFoundException
 from conans.model.manifest import FileTreeManifest
 from conans.model.profile import Profile
 from conans.model.ref import ConanFileReference, PackageReference
 from conans.model.settings import Settings
+from conans.server.revision_list import _RevisionEntry
 from conans.test.utils.runner import TestRunner
 from conans.test.utils.server_launcher import (TESTING_REMOTE_PRIVATE_PASS,
                                                TESTING_REMOTE_PRIVATE_USER,
@@ -56,6 +58,9 @@ from conans.util.files import mkdir, save, save_files
 from conans.util.log import logger
 
 NO_SETTINGS_PACKAGE_ID = "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9"
+ARTIFACTORY_DEFAULT_USER = "admin"
+ARTIFACTORY_DEFAULT_PASSWORD = "password"
+ARTIFACTORY_DEFAULT_URL = "http://localhost:8090/artifactory"
 
 
 def inc_recipe_manifest_timestamp(cache, reference, inc_time):
@@ -224,6 +229,142 @@ class TestRequester(object):
             kwargs["headers"].update(mock_request.headers)
 
 
+class ArtifactoryServerStore(object):
+
+    def __init__(self, repo_url, user, password):
+        self._user = user or ARTIFACTORY_DEFAULT_USER
+        self._password = password or ARTIFACTORY_DEFAULT_PASSWORD
+        self._repo_url = repo_url
+
+    @property
+    def _auth(self):
+        return self._user, self._password
+
+    @staticmethod
+    def _root_recipe(ref):
+        return "{}/{}/{}/{}".format(ref.user, ref.name, ref.version, ref.channel)
+
+    @staticmethod
+    def _ref_index(ref):
+        return "{}/index.json".format(ArtifactoryServerStore._root_recipe(ref))
+
+    @staticmethod
+    def _pref_index(pref):
+        tmp = ArtifactoryServerStore._root_recipe(pref.ref)
+        return "{}/{}/package/{}/index.json".format(tmp, pref.ref.revision, pref.id)
+
+    def get_recipe_revisions(self, ref):
+        time.sleep(0.1)  # Index appears to not being updated immediately after a remove
+        url = "{}/{}".format(self._repo_url, self._ref_index(ref))
+        response = requests.get(url, auth=self._auth)
+        response.raise_for_status()
+        the_json = response.json()
+        if not the_json["revisions"]:
+            raise RecipeNotFoundException(ref)
+        tmp = [_RevisionEntry(i["revision"], i["time"]) for i in the_json["revisions"]]
+        tmp.reverse()
+        return tmp
+
+    def get_package_revisions(self, pref):
+        time.sleep(0.1)  # Index appears to not being updated immediately
+        url = "{}/{}".format(self._repo_url, self._pref_index(pref))
+        response = requests.get(url, auth=self._auth)
+        response.raise_for_status()
+        the_json = response.json()
+        if not the_json["revisions"]:
+            raise PackageNotFoundException(pref)
+        tmp = [_RevisionEntry(i["revision"], i["time"]) for i in the_json["revisions"]]
+        tmp.reverse()
+        return tmp
+
+    def get_last_revision(self, ref):
+        revisions = self.get_recipe_revisions(ref)
+        return revisions[-1]
+
+    def get_last_package_revision(self, ref):
+        revisions = self.get_package_revisions(ref)
+        return revisions[-1]
+
+    def package_exists(self, pref):
+        try:
+            if pref.revision:
+                path = self.server_store.package(pref)
+            else:
+                path = self.test_server.server_store.package_revisions_root(pref)
+            return self.test_server.server_store.path_exists(path)
+        except NotFoundException:  # When resolves the latest and there is no package
+            return False
+
+
+class ArtifactoryServer(object):
+
+    def __init__(self, url=None, user=None, password=None, server_capabilities=None):
+        self._user = user or ARTIFACTORY_DEFAULT_USER
+        self._password = password or ARTIFACTORY_DEFAULT_PASSWORD
+        self._url = url or ARTIFACTORY_DEFAULT_URL
+        self._repo_name = "conan_{}".format(str(uuid.uuid4()).replace("-", ""))
+        self.create_repository()
+        self.server_store = ArtifactoryServerStore(self.repo_url, self._user, self._password)
+        if server_capabilities is not None:
+            raise nose.SkipTest("The Artifactory Server can't adjust capabilities")
+
+    @property
+    def _auth(self):
+        return self._user, self._password
+
+    @property
+    def repo_url(self):
+        return "{}/{}".format(self._url, self._repo_name)
+
+    @property
+    def repo_api_url(self):
+        return "{}/api/conan/{}".format(self._url, self._repo_name)
+
+    def recipe_revision_time(self, ref):
+        revs = self.server_store.get_recipe_revisions(ref)
+        for r in revs:
+            if r.revision == ref.revision:
+                return r.time
+        return None
+
+    def package_revision_time(self, pref):
+        revs = self.server_store.get_package_revisions(pref)
+        for r in revs:
+            if r.revision == pref.revision:
+                return r.time
+        return None
+
+    def create_repository(self):
+        url = "{}/api/repositories/{}".format(self._url, self._repo_name)
+        config = {"key": self._repo_name, "rclass": "local", "packageType": "conan"}
+        ret = requests.put(url, auth=self._auth, json=config)
+        ret.raise_for_status()
+
+    def package_exists(self, pref):
+        try:
+            revisions = self.server_store.get_package_revisions(pref)
+            if pref.revision:
+                for r in revisions:
+                    if pref.revision == r.revision:
+                        return True
+                return False
+            return True
+        except Exception:  # When resolves the latest and there is no package
+            return False
+
+    def recipe_exists(self, ref):
+        try:
+            revisions = self.server_store.get_recipe_revisions(ref)
+            if ref.revision:
+                for r in revisions:
+                    if ref.revision == r.revision:
+                        return True
+                return False
+            return True
+        except Exception:  # When resolves the latest and there is no package
+            return False
+
+
 class TestServer(object):
     def __init__(self, read_permissions=None,
                  write_permissions=None, users=None, plugins=None, base_path=None,
@@ -306,6 +447,10 @@ class TestServer(object):
         return tmp
 
 
+if get_env("CONAN_TEST_WITH_ARTIFACTORY", False):
+    TestServer = ArtifactoryServer
+
+
 class TestBufferConanOutput(ConanOutput):
 
     """ wraps the normal output of the application, captures it into an stream
@@ -582,7 +727,11 @@ servers["r2"] = TestServer()
         registry = self.cache.registry
 
         def add_server_to_registry(name, server):
-            if isinstance(server, TestServer):
+            if isinstance(server, ArtifactoryServer):
+                registry.remotes.add(name, server.repo_api_url)
+                self.users.update({name: [(ARTIFACTORY_DEFAULT_USER,
+                                           ARTIFACTORY_DEFAULT_PASSWORD)]})
+            elif isinstance(server, TestServer):
                 registry.remotes.add(name, server.fake_url)
             else:
                 registry.remotes.add(name, server)
@@ -626,8 +775,9 @@ servers["r2"] = TestServer()
         # Check if servers are real
         real_servers = False
         for server in self.servers.values():
-            if isinstance(server, str):  # Just URI
+            if isinstance(server, str) or isinstance(server, ArtifactoryServer):  # Just URI
                 real_servers = True
+                break
 
         with tools.environment_append(self.cache.config.env_vars):
             if real_servers: