Repository: dwavesystems/dwave-cloud-client

Issue:
Ping should respect solver's graph
Currently, a fixed Ising problem `({0: 1}, {})` is always submitted, obviously ignoring the actual solver's graph.

Note that `sample --random-problem` respects the graph structure.

Discussion:

Patch:
diff --git a/dwave/cloud/cli.py b/dwave/cloud/cli.py
index f11bdb2..af6ecc7 100644
--- a/dwave/cloud/cli.py
+++ b/dwave/cloud/cli.py
@@ -234,11 +234,18 @@ def _ping(config_file, profile, solver_def, request_timeout, polling_timeout, ou
     except Exception as e:
         raise CLIError("Unexpected error while fetching solver: {!r}".format(e), 5)
 
+    if hasattr(solver, 'nodes'):
+        # structured solver: use the first existing node
+        problem = ({min(solver.nodes): 0}, {})
+    else:
+        # unstructured solver doesn't constrain problem graph
+        problem = ({0: 1}, {})
+
     t1 = timer()
     output("Using solver: {solver_id}", solver_id=solver.id)
 
     try:
-        future = solver.sample_ising({0: 1}, {})
+        future = solver.sample_ising(*problem)
         timing = future.timing
     except RequestTimeout:
         raise CLIError("API connection timed out.", 8)
diff --git a/dwave/cloud/utils.py b/dwave/cloud/utils.py
index 0608f67..802684a 100644
--- a/dwave/cloud/utils.py
+++ b/dwave/cloud/utils.py
@@ -486,10 +486,13 @@ class tictoc(object):
         self.dt = perf_counter() - self.tick
 
 
-def parse_loglevel(level_name):
+def parse_loglevel(level_name, default=logging.NOTSET):
     """Resolve numeric and symbolic log level names to numeric levels."""
 
-    level_name = (level_name or '').strip().lower()
+    try:
+        level_name = str(level_name or '').strip().lower()
+    except:
+        return default
 
     # note: make sure `TRACE` level is added to `logging` before calling this
     known_levels = {
@@ -507,7 +510,7 @@ def parse_loglevel(level_name):
     try:
         level = int(level_name)
     except ValueError:
-        level = known_levels.get(level_name, logging.NOTSET)
+        level = known_levels.get(level_name, default)
 
     return level
 


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ec342e5..0e9c136 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -180,6 +180,9 @@ class TestCli(unittest.TestCase):
         profile = 'profile'
 
         with mock.patch('dwave.cloud.cli.Client') as m:
+            # mock returned solver
+            client = m.from_config.return_value
+            client.get_solver.return_value.nodes = [5, 7, 3]
 
             runner = CliRunner()
             with runner.isolated_filesystem():
@@ -196,12 +199,11 @@ class TestCli(unittest.TestCase):
                 request_timeout=0.5, polling_timeout=30)
 
             # get solver called?
-            c = m.from_config.return_value
-            c.get_solver.assert_called_with()
+            client.get_solver.assert_called_with()
 
             # sampling method called on solver?
-            s = c.get_solver.return_value
-            s.sample_ising.assert_called_with({0: 1}, {})
+            solver = client.get_solver.return_value
+            solver.sample_ising.assert_called_with({3: 0}, {})
 
         self.assertEqual(result.exit_code, 0)
 
diff --git a/tests/test_mock_solver_loading.py b/tests/test_mock_solver_loading.py
index 158462b..953c7cd 100644
--- a/tests/test_mock_solver_loading.py
+++ b/tests/test_mock_solver_loading.py
@@ -47,8 +47,8 @@ def structured_solver_data(id_, incomplete=False):
     obj = {
         "properties": {
             "supported_problem_types": ["qubo", "ising"],
-            "qubits": [0, 1, 2],
-            "couplers": [[0, 1], [0, 2], [1, 2]],
+            "qubits": [1, 2, 3],
+            "couplers": [[1, 2], [1, 3], [2, 3]],
             "num_qubits": 3,
             "parameters": {"num_reads": "Number of samples to return."}
         },
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a3cecf5..8a2f7d1 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import logging
 import unittest
 from collections import OrderedDict
 from itertools import count
@@ -20,7 +21,7 @@ from datetime import datetime
 from dwave.cloud.utils import (
     uniform_iterator, uniform_get, strip_head, strip_tail,
     active_qubits, generate_random_ising_problem,
-    default_text_input, utcnow, cached, retried)
+    default_text_input, utcnow, cached, retried, parse_loglevel)
 from dwave.cloud.testing import mock
 
 
@@ -118,6 +119,25 @@ class TestSimpleUtils(unittest.TestCase):
         unaware = t.replace(tzinfo=None)
         self.assertLess((now - unaware).total_seconds(), 1.0)
 
+    def test_parse_loglevel_invalid(self):
+        """Parsing invalid log levels returns NOTSET."""
+        notset = logging.NOTSET
+
+        self.assertEqual(parse_loglevel(''), notset)
+        self.assertEqual(parse_loglevel('  '), notset)
+        self.assertEqual(parse_loglevel(None), notset)
+        self.assertEqual(parse_loglevel(notset), notset)
+        self.assertEqual(parse_loglevel('nonexisting'), notset)
+        self.assertEqual(parse_loglevel({'a': 1}), notset)
+        self.assertIsNone(parse_loglevel('nonexisting', default=None))
+
+    def test_parse_loglevel_numeric_and_symbolic(self):
+        self.assertEqual(parse_loglevel('info'), logging.INFO)
+        self.assertEqual(parse_loglevel('INFO'), logging.INFO)
+        self.assertEqual(parse_loglevel(logging.INFO), logging.INFO)
+        self.assertEqual(parse_loglevel(str(logging.INFO)), logging.INFO)
+        self.assertEqual(parse_loglevel('  %d  ' % logging.INFO), logging.INFO)
+
 
 class TestCachedDecorator(unittest.TestCase):