diff --git a/meta/lib/oeqa/sdk/case.py b/meta/lib/oeqa/sdk/case.py
index 03cfde88ff42..5f269161f4e1 100644
--- a/meta/lib/oeqa/sdk/case.py
+++ b/meta/lib/oeqa/sdk/case.py
@@ -18,38 +18,80 @@ class OESDKTestCase(OETestCase):
                 (self.tc.sdk_env, cmd), shell=True, executable="/bin/bash",
                 stderr=subprocess.STDOUT, universal_newlines=True)
 
-    def ensure_host_package(self, *packages, recipe=None):
+    def find_sdk_binary(self, binaries):
         """
-        Check that the host variation of one of the packages listed is available
-        in the SDK (nativesdk-foo for SDK, foo-native for eSDK). The package is
-        a list for the case where debian-renaming may have occured, and the
-        manifest could contain 'foo' or 'libfoo'.
+        (name, outside) for the first of binaries that resolves.
 
-        If testing an eSDK and the package is not found, then try to install the
-        specified recipe to install it from sstate.
+        name is the command to run when it came from the SDK, and outside
+        lists what resolved beyond it otherwise. Callers run the name
+        returned: an eSDK stages native recipes, which need not name a
+        command the way their nativesdk counterparts do. perl-native keeps
+        its interpreter at bindir/perl-native/perl, reached as nativeperl.
+        """
+        if isinstance(binaries, str):
+            binaries = (binaries,)
+        sdk_root = os.path.dirname(self.tc.sdk_env)
+
+        outside = []
+        for binary in binaries:
+            try:
+                path = self._run("command -v %s" % binary).strip()
+            except subprocess.CalledProcessError:
+                continue
+            if path.startswith(sdk_root + os.sep):
+                return binary, []
+            outside.append("%s is %s" % (binary, path))
+        return None, outside
+
+    def ensure_sdk_binary(self, binaries, packages=()):
         """
+        Return the first of binaries that resolves inside the SDK.
 
-        # In a SDK the manifest is correct. In an eSDK the manifest may be
-        # correct (type=full) or not include packages that exist in sstate but
-        # not installed yet (minimal) so we should try to install the recipe.
-        for package in packages:
-            if isinstance(self.tc, OESDKExtTestContext):
-                package = package + "-native"
-            else:
-                package = "nativesdk-" + package
+        Fail when one resolves outside it, because a result from the build
+        host describes the host and not the SDK. Skip when none resolves.
+        """
+        if isinstance(binaries, str):
+            binaries = (binaries,)
+        if isinstance(packages, str):
+            packages = (packages,)
+        name, outside = self.find_sdk_binary(binaries)
+        if name:
+            return name
+        need = ", ".join(packages) or ", ".join(binaries)
+        if outside:
+            self.fail("Test %s needs one of %s from the SDK, but found %s"
+                      % (self.id(), need, "; ".join(outside)))
+        raise unittest.SkipTest("Test %s needs one of %s: the SDK has no %s"
+                                % (self.id(), need, " or ".join(binaries)))
+
+    def ensure_host_package(self, *packages, recipe=None, binary=None):
+        """
+        Return the command the test should run, having confirmed it comes
+        from the SDK rather than the build host.
 
-            if self.tc.hasHostPackage(package):
-                break
-        else:
-            if isinstance(self.tc, OESDKExtTestContext):
-                recipe = (recipe or packages[0]) + "-native"
-                print("Trying to install %s..." % recipe)
-                try:
-                    self._run('devtool sdk-install %s' % recipe)
-                except subprocess.CalledProcessError:
-                    raise unittest.SkipTest("Test %s needs one of %s" % (self.id(), ", ".join(packages)))
-            else:
-                raise unittest.SkipTest("Test %s needs one of %s" % (self.id(), ", ".join(packages)))
+        An eSDK ships sstate for more than it installs, so a tool can be
+        available without being present. When the command is missing, ask
+        devtool to install the recipe and look again.
+
+        binary names the commands the packages provide, and is needed only
+        where a command is not named after its package: pkgconf provides
+        pkg-config, and an eSDK reaches perl as nativeperl.
+        """
+        binaries = binary or packages
+
+        name, _ = self.find_sdk_binary(binaries)
+        if name:
+            return name
+
+        if isinstance(self.tc, OESDKExtTestContext):
+            install = (recipe or packages[0]) + "-native"
+            print("Trying to install %s..." % install)
+            try:
+                self._run('devtool sdk-install %s' % install)
+            except subprocess.CalledProcessError:
+                pass
+
+        return self.ensure_sdk_binary(binaries, packages)
 
     def ensure_target_package(self, *packages, multilib=False, recipe=None):
         """
