buffet: Use command schema to define "progress"

According to the GCD specification command progress is dictionary,
defined same way as parameters and results.
Mapping between "done" and "progress" dictionary is not obvious, so patch
removes updates of progress from CommandInstance::Done().

BUG=brillo:915
TEST=FEATURES=test emerge-gizmo buffet

Change-Id: If6f6d52dbc0595a8f1cf0cf949a741675c9e5f3c
Reviewed-on: https://chromium-review.googlesource.com/268352
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Vitaly Buka <vitalybuka@chromium.org>
Tested-by: Vitaly Buka <vitalybuka@chromium.org>
diff --git a/buffet/buffet_client.cc b/buffet/buffet_client.cc
index 675b4dc..a874ec0 100644
--- a/buffet/buffet_client.cc
+++ b/buffet/buffet_client.cc
@@ -373,11 +373,8 @@
   void CallGetPendingCommands() {
     printf("Pending commands:\n");
     for (auto* cmd : object_manager_->GetCommandInstances()) {
-      printf("%10s (%-3d) - '%s' (id:%s)\n",
-             cmd->status().c_str(),
-             cmd->progress(),
-             cmd->name().c_str(),
-             cmd->id().c_str());
+      printf("%10s - '%s' (id:%s)\n", cmd->status().c_str(),
+             cmd->name().c_str(), cmd->id().c_str());
     }
     OnJobComplete();
   }
diff --git a/buffet/commands/cloud_command_proxy.cc b/buffet/commands/cloud_command_proxy.cc
index e86ee8f..78f87cb 100644
--- a/buffet/commands/cloud_command_proxy.cc
+++ b/buffet/commands/cloud_command_proxy.cc
@@ -65,8 +65,8 @@
   }
 
   if (new_pending_command_updates_.test(kFlagProgress)) {
-    patch.Set(commands::attributes::kCommand_Progress,
-              command_instance_->GetProgressJson().release());
+    auto json = TypedValueToJson(command_instance_->GetProgress(), nullptr);
+    patch.Set(commands::attributes::kCommand_Progress, json.release());
   }
   command_update_in_progress_ = true;
   in_progress_command_updates_ = new_pending_command_updates_;
diff --git a/buffet/commands/command_definition.cc b/buffet/commands/command_definition.cc
index 02b7722..260cb3d 100644
--- a/buffet/commands/command_definition.cc
+++ b/buffet/commands/command_definition.cc
@@ -58,10 +58,12 @@
 CommandDefinition::CommandDefinition(
     const std::string& category,
     std::unique_ptr<const ObjectSchema> parameters,
+    std::unique_ptr<const ObjectSchema> progress,
     std::unique_ptr<const ObjectSchema> results)
-        : category_{category},
-          parameters_{std::move(parameters)},
-          results_{std::move(results)} {
+    : category_{category},
+      parameters_{std::move(parameters)},
+      progress_{std::move(progress)},
+      results_{std::move(results)} {
   // Set to be available to all clients by default.
   visibility_ = Visibility::GetAll();
 }
diff --git a/buffet/commands/command_definition.h b/buffet/commands/command_definition.h
index 74ec09d..3328a94 100644
--- a/buffet/commands/command_definition.h
+++ b/buffet/commands/command_definition.h
@@ -46,12 +46,15 @@
 
   CommandDefinition(const std::string& category,
                     std::unique_ptr<const ObjectSchema> parameters,
+                    std::unique_ptr<const ObjectSchema> progress,
                     std::unique_ptr<const ObjectSchema> results);
 
   // Gets the category this command belongs to.
   const std::string& GetCategory() const { return category_; }
   // Gets the object schema for command parameters.
   const ObjectSchema* GetParameters() const { return parameters_.get(); }
+  // Gets the object schema for command progress.
+  const ObjectSchema* GetProgress() const { return progress_.get(); }
   // Gets the object schema for command results.
   const ObjectSchema* GetResults() const { return results_.get(); }
   // Returns the command visibility.
@@ -62,6 +65,7 @@
  private:
   std::string category_;  // Cmd category. Could be "powerd" for "base.reboot".
   std::unique_ptr<const ObjectSchema> parameters_;  // Command parameters def.
+  std::unique_ptr<const ObjectSchema> progress_;    // Command progress def.
   std::unique_ptr<const ObjectSchema> results_;  // Command results def.
   Visibility visibility_;  // Available to all by default.
 
diff --git a/buffet/commands/command_definition_unittest.cc b/buffet/commands/command_definition_unittest.cc
index 0254fd9..171164d 100644
--- a/buffet/commands/command_definition_unittest.cc
+++ b/buffet/commands/command_definition_unittest.cc
@@ -75,12 +75,16 @@
 
 TEST(CommandDefinition, Test) {
   std::unique_ptr<const ObjectSchema> params{ObjectSchema::Create()};
+  std::unique_ptr<const ObjectSchema> progress{ObjectSchema::Create()};
   std::unique_ptr<const ObjectSchema> results{ObjectSchema::Create()};
   const ObjectSchema* param_ptr = params.get();
+  const ObjectSchema* progress_ptr = progress.get();
   const ObjectSchema* results_ptr = results.get();
-  CommandDefinition def{"powerd", std::move(params), std::move(results)};
+  CommandDefinition def{
+      "powerd", std::move(params), std::move(progress), std::move(results)};
   EXPECT_EQ("powerd", def.GetCategory());
   EXPECT_EQ(param_ptr, def.GetParameters());
+  EXPECT_EQ(progress_ptr, def.GetProgress());
   EXPECT_EQ(results_ptr, def.GetResults());
   EXPECT_EQ("all", def.GetVisibility().ToString());
 
diff --git a/buffet/commands/command_dictionary.cc b/buffet/commands/command_dictionary.cc
index cbfd46b..a73d73f 100644
--- a/buffet/commands/command_dictionary.cc
+++ b/buffet/commands/command_dictionary.cc
@@ -68,6 +68,7 @@
           chromeos::string_utils::Join(".", package_name, command_name);
 
       const ObjectSchema* base_parameters_def = nullptr;
+      const ObjectSchema* base_progress_def = nullptr;
       const ObjectSchema* base_results_def = nullptr;
       // By default make it available to all clients.
       auto visibility = CommandDefinition::Visibility::GetAll();
@@ -75,6 +76,7 @@
         auto cmd = base_commands->FindCommand(full_command_name);
         if (cmd) {
           base_parameters_def = cmd->GetParameters();
+          base_progress_def = cmd->GetProgress();
           base_results_def = cmd->GetResults();
           visibility = cmd->GetVisibility();
         }
@@ -105,6 +107,12 @@
       if (!parameters_schema)
         return false;
 
+      auto progress_schema = BuildObjectSchema(
+          command_def_json, commands::attributes::kCommand_Progress,
+          base_progress_def, full_command_name, error);
+      if (!progress_schema)
+        return false;
+
       auto results_schema = BuildObjectSchema(
           command_def_json,
           commands::attributes::kCommand_Results,
@@ -127,9 +135,10 @@
       }
 
       std::unique_ptr<CommandDefinition> command_def{
-        new CommandDefinition{category, std::move(parameters_schema),
-                              std::move(results_schema)}
-      };
+          new CommandDefinition{category,
+                                std::move(parameters_schema),
+                                std::move(progress_schema),
+                                std::move(results_schema)}};
       command_def->SetVisibility(visibility);
       new_defs.emplace(full_command_name, std::move(command_def));
 
diff --git a/buffet/commands/command_dictionary_unittest.cc b/buffet/commands/command_dictionary_unittest.cc
index 63492cc..914bdd3 100644
--- a/buffet/commands/command_dictionary_unittest.cc
+++ b/buffet/commands/command_dictionary_unittest.cc
@@ -25,6 +25,9 @@
           'height': 'integer',
           '_jumpType': ['_withAirFlip', '_withSpin', '_withKick']
         },
+        'progress': {
+          'progress': 'integer'
+        },
         'results': {}
       }
     }
diff --git a/buffet/commands/command_instance.cc b/buffet/commands/command_instance.cc
index e62e048..24c7869 100644
--- a/buffet/commands/command_instance.cc
+++ b/buffet/commands/command_instance.cc
@@ -151,10 +151,10 @@
   json->SetString(commands::attributes::kCommand_Name, name_);
   json->Set(commands::attributes::kCommand_Parameters,
             TypedValueToJson(parameters_, nullptr).release());
+  json->Set(commands::attributes::kCommand_Progress,
+            TypedValueToJson(progress_, nullptr).release());
   json->Set(commands::attributes::kCommand_Results,
             TypedValueToJson(results_, nullptr).release());
-  json->Set(commands::attributes::kCommand_Progress,
-            GetProgressJson().release());
   json->SetString(commands::attributes::kCommand_State, status_);
 
   return json;
@@ -175,12 +175,11 @@
   return true;
 }
 
-bool CommandInstance::SetProgress(int progress) {
-  if (progress < 0 || progress > 100)
-    return false;
+bool CommandInstance::SetProgress(const native_types::Object& progress) {
+  // Change status even if progress unchanged, e.g. 0% -> 0%.
+  SetStatus(kStatusInProgress);
   if (progress != progress_) {
     progress_ = progress;
-    SetStatus(kStatusInProgress);
     for (auto& proxy : proxies_) {
       proxy->OnProgressChanged();
     }
@@ -201,7 +200,6 @@
 }
 
 void CommandInstance::Done() {
-  SetProgress(100);
   SetStatus(kStatusDone);
   RemoveFromQueue();
   // The command will be destroyed after that, so do not access any members.
@@ -221,15 +219,4 @@
     queue_->DelayedRemove(GetID());
 }
 
-std::unique_ptr<base::Value> CommandInstance::GetProgressJson() const {
-  // GCD server requires "progress" to be a JSON object. We will just make
-  // an object with a single field, "progress", so the patch request will
-  // look like this: {"progress": {"progress":100}}.
-  std::unique_ptr<base::DictionaryValue> progress_object{
-      new base::DictionaryValue};
-  progress_object->SetInteger(commands::attributes::kCommand_Progress,
-                              progress_);
-  return std::move(progress_object);
-}
-
 }  // namespace buffet
diff --git a/buffet/commands/command_instance.h b/buffet/commands/command_instance.h
index db9c519..f67b30b 100644
--- a/buffet/commands/command_instance.h
+++ b/buffet/commands/command_instance.h
@@ -81,13 +81,14 @@
   // Sets the pointer to queue this command is part of.
   void SetCommandQueue(CommandQueue* queue) { queue_ = queue; }
 
+  // Updates the command progress. The |progress| should match the schema.
+  // Returns false if |results| value is incorrect.
+  bool SetProgress(const native_types::Object& progress);
+
   // Updates the command results. The |results| should match the schema.
   // Returns false if |results| value is incorrect.
   bool SetResults(const native_types::Object& results);
 
-  // Updates the command execution progress. The |progress| must be between
-  // 0 and 100. Returns false if |progress| value is incorrect.
-  bool SetProgress(int progress);
   // Aborts command execution.
   void Abort();
   // Cancels command execution.
@@ -96,12 +97,9 @@
   void Done();
 
   // Command state getters.
-  int GetProgress() const { return progress_; }
+  const native_types::Object& GetProgress() const { return progress_; }
   const std::string& GetStatus() const { return status_; }
 
-  // Returns the "progress" portion of JSON command resource.
-  std::unique_ptr<base::Value> GetProgressJson() const;
-
   // Values for command execution status.
   static const char kStatusQueued[];
   static const char kStatusInProgress[];
@@ -131,12 +129,12 @@
   const CommandDefinition* command_definition_;
   // Command parameters and their values.
   native_types::Object parameters_;
+  // Current command execution progress.
+  native_types::Object progress_;
   // Command results.
   native_types::Object results_;
   // Current command status.
   std::string status_ = kStatusQueued;
-  // Current command execution progress.
-  int progress_ = 0;
   // Command proxies for the command.
   std::vector<std::unique_ptr<CommandProxyInterface>> proxies_;
   // Pointer to the command queue this command instance is added to.
diff --git a/buffet/commands/command_instance_unittest.cc b/buffet/commands/command_instance_unittest.cc
index e4fb288..8d7d0ec 100644
--- a/buffet/commands/command_instance_unittest.cc
+++ b/buffet/commands/command_instance_unittest.cc
@@ -208,12 +208,14 @@
   })");
   auto instance =
       CommandInstance::FromJson(json.get(), "cloud", dict_, nullptr);
-  instance->SetProgress(15);
+  instance->SetProgress(
+      native_types::Object{{"progress", unittests::make_int_prop_value(15)}});
+  instance->SetProgress(
+      native_types::Object{{"progress", unittests::make_int_prop_value(15)}});
   instance->SetID("testId");
   native_types::Object results;
-  IntPropType int_prop;
-  results["testResult"] = int_prop.CreateValue(17, nullptr);
-  instance->SetResults(results);
+  instance->SetResults(
+      native_types::Object{{"testResult", unittests::make_int_prop_value(17)}});
 
   json->MergeDictionary(CreateDictionaryValue(R"({
     'id': 'testId',
diff --git a/buffet/commands/command_queue_unittest.cc b/buffet/commands/command_queue_unittest.cc
index ba152b7..8829404 100644
--- a/buffet/commands/command_queue_unittest.cc
+++ b/buffet/commands/command_queue_unittest.cc
@@ -40,6 +40,7 @@
  private:
   CommandDefinition command_definition_{"powerd",
                                         ObjectSchema::Create(),
+                                        ObjectSchema::Create(),
                                         ObjectSchema::Create()};
 };
 
diff --git a/buffet/commands/dbus_command_dispatcher_unittest.cc b/buffet/commands/dbus_command_dispatcher_unittest.cc
index 313bc28..ffd4394 100644
--- a/buffet/commands/dbus_command_dispatcher_unittest.cc
+++ b/buffet/commands/dbus_command_dispatcher_unittest.cc
@@ -79,7 +79,8 @@
         },
         'shutdown': {
           'parameters': {},
-          'results': {}
+          'results': {},
+          'progress': {'progress': 'integer'}
         }
       }
     })");
@@ -121,8 +122,9 @@
     proxy->Done();
   }
 
-  void SetProgress(DBusCommandProxy* proxy, int progress) {
-    proxy->SetProgress(nullptr, progress);
+  void SetProgress(DBusCommandProxy* proxy,
+                   const native_types::Object& progress) {
+    EXPECT_TRUE(proxy->SetProgress(nullptr, ObjectToDBusVariant(progress)));
   }
 
 
@@ -146,14 +148,16 @@
 
   // Two properties are set, Progress = 50%, Status = "inProgress"
   EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
-  SetProgress(command_proxy, 50);
+  native_types::Object progress{
+      {"progress", unittests::make_int_prop_value(50)}};
+  SetProgress(command_proxy, progress);
   EXPECT_EQ(CommandInstance::kStatusInProgress, command_instance->GetStatus());
-  EXPECT_EQ(50, command_instance->GetProgress());
+  EXPECT_EQ(progress, command_instance->GetProgress());
 
   // Command must be removed from the queue and proxy destroyed after calling
   // FinishCommand().
-  // Two properties are set, Progress = 100%, Status = "done"
-  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
+  // One property is set, Status = "done"
+  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(1);
   // D-Bus command proxy is going away.
   EXPECT_CALL(*mock_exported_command_proxy_, Unregister()).Times(1);
   // Two interfaces are being removed on the D-Bus command object.
@@ -177,16 +181,17 @@
   ASSERT_NE(nullptr, command_proxy);
   EXPECT_EQ(CommandInstance::kStatusQueued, command_instance->GetStatus());
 
-  // Two properties are set, Progress = 50%, Status = "inProgress"
-  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
-  SetProgress(command_proxy, 50);
+  // One property is set, Status = "inProgress"
+  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(1);
+  native_types::Object progress{};
+  SetProgress(command_proxy, progress);
   EXPECT_EQ(CommandInstance::kStatusInProgress, command_instance->GetStatus());
-  EXPECT_EQ(50, command_instance->GetProgress());
+  EXPECT_EQ(progress, command_instance->GetProgress());
 
   // Command must be removed from the queue and proxy destroyed after calling
   // FinishCommand().
-  // Two properties are set, Progress = 100%, Status = "done"
-  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
+  // One property is set, Status = "done"
+  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(1);
   // D-Bus command proxy is going away.
   EXPECT_CALL(*mock_exported_command_proxy_, Unregister()).Times(1);
   // Two interfaces are being removed on the D-Bus command object.
diff --git a/buffet/commands/dbus_command_proxy.cc b/buffet/commands/dbus_command_proxy.cc
index 33f197d..74278fe 100644
--- a/buffet/commands/dbus_command_proxy.cc
+++ b/buffet/commands/dbus_command_proxy.cc
@@ -35,7 +35,8 @@
   dbus_adaptor_.SetCategory(command_instance_->GetCategory());
   dbus_adaptor_.SetId(command_instance_->GetID());
   dbus_adaptor_.SetStatus(command_instance_->GetStatus());
-  dbus_adaptor_.SetProgress(command_instance_->GetProgress());
+  dbus_adaptor_.SetProgress(
+      ObjectToDBusVariant(command_instance_->GetProgress()));
   dbus_adaptor_.SetOrigin(command_instance_->GetOrigin());
 
   dbus_adaptor_.SetParameters(ObjectToDBusVariant(
@@ -57,22 +58,23 @@
 }
 
 void DBusCommandProxy::OnProgressChanged() {
-  dbus_adaptor_.SetProgress(command_instance_->GetProgress());
+  dbus_adaptor_.SetProgress(
+      ObjectToDBusVariant(command_instance_->GetProgress()));
 }
 
-bool DBusCommandProxy::SetProgress(chromeos::ErrorPtr* error,
-                                   int32_t progress) {
-  LOG(INFO) << "Received call to Command<"
-            << command_instance_->GetName() << ">::SetProgress("
-            << progress << ")";
+bool DBusCommandProxy::SetProgress(
+    chromeos::ErrorPtr* error,
+    const chromeos::VariantDictionary& progress) {
+  LOG(INFO) << "Received call to Command<" << command_instance_->GetName()
+            << ">::SetProgress()";
 
-  // Validate |progress| parameter. Its value must be between 0 and 100.
-  IntPropType progress_type;
-  progress_type.AddMinMaxConstraint(0, 100);
-  if (!progress_type.ValidateValue(progress, error))
+  auto progress_schema =
+      command_instance_->GetCommandDefinition()->GetProgress();
+  native_types::Object obj;
+  if (!ObjectFromDBusVariant(progress_schema, progress, &obj, error))
     return false;
 
-  command_instance_->SetProgress(progress);
+  command_instance_->SetProgress(obj);
   return true;
 }
 
diff --git a/buffet/commands/dbus_command_proxy.h b/buffet/commands/dbus_command_proxy.h
index 178860a..aac671c 100644
--- a/buffet/commands/dbus_command_proxy.h
+++ b/buffet/commands/dbus_command_proxy.h
@@ -44,7 +44,8 @@
 
  private:
   // Handles calls to org.chromium.Buffet.Command.SetProgress(progress).
-  bool SetProgress(chromeos::ErrorPtr* error, int32_t progress) override;
+  bool SetProgress(chromeos::ErrorPtr* error,
+                   const chromeos::VariantDictionary& progress) override;
   // Handles calls to org.chromium.Buffet.Command.SetResults(results).
   bool SetResults(chromeos::ErrorPtr* error,
                   const chromeos::VariantDictionary& results) override;
diff --git a/buffet/commands/dbus_command_proxy_unittest.cc b/buffet/commands/dbus_command_proxy_unittest.cc
index 45d4b78..e0cad8f 100644
--- a/buffet/commands/dbus_command_proxy_unittest.cc
+++ b/buffet/commands/dbus_command_proxy_unittest.cc
@@ -71,6 +71,13 @@
             'bar': {
               'type': 'string'
             }
+          },
+          'progress': {
+            'progress': {
+              'type': 'integer',
+              'minimum': 0,
+              'maximum': 100
+            }
           }
         }
       }
@@ -141,31 +148,31 @@
     {"height", int32_t{53}},
     {"_jumpType", std::string{"_withKick"}},
   };
-  VariantDictionary results;
-
   EXPECT_EQ(CommandInstance::kStatusQueued, GetCommandAdaptor()->GetStatus());
-  EXPECT_EQ(0, GetCommandAdaptor()->GetProgress());
   EXPECT_EQ(params, GetCommandAdaptor()->GetParameters());
-  EXPECT_EQ(results, GetCommandAdaptor()->GetResults());
+  EXPECT_EQ(VariantDictionary{}, GetCommandAdaptor()->GetProgress());
+  EXPECT_EQ(VariantDictionary{}, GetCommandAdaptor()->GetResults());
   EXPECT_EQ("robot.jump", GetCommandAdaptor()->GetName());
   EXPECT_EQ(kTestCommandCategoty, GetCommandAdaptor()->GetCategory());
   EXPECT_EQ(kTestCommandId, GetCommandAdaptor()->GetId());
-  EXPECT_EQ(params, GetCommandAdaptor()->GetParameters());
-  EXPECT_EQ(results, GetCommandAdaptor()->GetResults());
 }
 
 TEST_F(DBusCommandProxyTest, SetProgress) {
   EXPECT_CALL(*mock_exported_object_command_, SendSignal(_)).Times(2);
-  EXPECT_TRUE(GetCommandInterface()->SetProgress(nullptr, 10));
+  EXPECT_TRUE(
+      GetCommandInterface()->SetProgress(nullptr, {{"progress", int32_t{10}}}));
   EXPECT_EQ(CommandInstance::kStatusInProgress,
             GetCommandAdaptor()->GetStatus());
-  EXPECT_EQ(10, GetCommandAdaptor()->GetProgress());
+
+  VariantDictionary progress{{"progress", int32_t{10}}};
+  EXPECT_EQ(progress, GetCommandAdaptor()->GetProgress());
 }
 
 TEST_F(DBusCommandProxyTest, SetProgress_OutOfRange) {
-  EXPECT_FALSE(GetCommandInterface()->SetProgress(nullptr, 110));
+  EXPECT_FALSE(GetCommandInterface()->SetProgress(
+      nullptr, {{"progress", int32_t{110}}}));
   EXPECT_EQ(CommandInstance::kStatusQueued, GetCommandAdaptor()->GetStatus());
-  EXPECT_EQ(0, GetCommandAdaptor()->GetProgress());
+  EXPECT_EQ(VariantDictionary{}, GetCommandAdaptor()->GetProgress());
 }
 
 TEST_F(DBusCommandProxyTest, SetResults) {
@@ -201,14 +208,11 @@
 }
 
 TEST_F(DBusCommandProxyTest, Done) {
-  // 3 property updates:
-  // status: queued -> inProgress
-  // progress: 0 -> 100
-  // status: inProgress -> done
-  EXPECT_CALL(*mock_exported_object_command_, SendSignal(_)).Times(3);
+  // 1 property update:
+  // status: queued -> done
+  EXPECT_CALL(*mock_exported_object_command_, SendSignal(_)).Times(1);
   GetCommandInterface()->Done();
   EXPECT_EQ(CommandInstance::kStatusDone, GetCommandAdaptor()->GetStatus());
-  EXPECT_EQ(100, GetCommandAdaptor()->GetProgress());
 }
 
 }  // namespace buffet
diff --git a/buffet/dbus_bindings/org.chromium.Buffet.Command.xml b/buffet/dbus_bindings/org.chromium.Buffet.Command.xml
index 89481ec..c9ed924 100644
--- a/buffet/dbus_bindings/org.chromium.Buffet.Command.xml
+++ b/buffet/dbus_bindings/org.chromium.Buffet.Command.xml
@@ -3,7 +3,7 @@
 <node xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
   <interface name="org.chromium.Buffet.Command">
     <method name="SetProgress">
-      <arg name="progress" type="i" direction="in"/>
+      <arg name="progress" type="a{sv}" direction="in"/>
       <annotation name="org.chromium.DBus.Method.Kind" value="normal"/>
     </method>
     <method name="SetResults">
@@ -34,8 +34,8 @@
     <property name="Category" type="s" access="read"/>
     <property name="Id" type="s" access="read"/>
     <property name="Status" type="s" access="read"/>
-    <property name="Progress" type="i" access="read"/>
     <property name="Parameters" type="a{sv}" access="read"/>
+    <property name="Progress" type="a{sv}" access="read"/>
     <property name="Results" type="a{sv}" access="read"/>
     <property name="Origin" type="s" access="read">
       <tp:docstring>
diff --git a/buffet/device_registration_info_unittest.cc b/buffet/device_registration_info_unittest.cc
index c833e5e..6e3feb2 100644
--- a/buffet/device_registration_info_unittest.cc
+++ b/buffet/device_registration_info_unittest.cc
@@ -566,7 +566,9 @@
                          chromeos::http::request_type::kPatch,
                          base::Bind(update_command_progress));
 
-  command->SetProgress(18);
+  native_types::Object progress{
+      {"progress", unittests::make_int_prop_value(18)}};
+  command->SetProgress(progress);
 
   // UpdateCommand when changing command status.
   auto update_command_state = [](const ServerRequest& request,
diff --git a/buffet/etc/buffet/commands/test.json b/buffet/etc/buffet/commands/test.json
index 182192f..aac439d 100644
--- a/buffet/etc/buffet/commands/test.json
+++ b/buffet/etc/buffet/commands/test.json
@@ -3,6 +3,13 @@
     "_jump": {
       "parameters": {
         "_height":"integer"
+      },
+      "progress": {
+        "progress": {
+          "type": "integer",
+          "minimum": 0,
+          "maximum": 100
+        }
       }
     }
   }
diff --git a/buffet/test_daemon/main.cc b/buffet/test_daemon/main.cc
index c208cd8..0da72c2 100644
--- a/buffet/test_daemon/main.cc
+++ b/buffet/test_daemon/main.cc
@@ -12,6 +12,8 @@
 #include <base/bind.h>
 #include <base/command_line.h>
 #include <base/format_macros.h>
+#include <base/json/json_writer.h>
+#include <base/values.h>
 #include <chromeos/daemons/dbus_daemon.h>
 #include <chromeos/map_utils.h>
 #include <chromeos/strings/string_utils.h>
@@ -21,6 +23,53 @@
 
 namespace {
 const char kTestCommandCategory[] = "test";
+
+std::unique_ptr<base::DictionaryValue> DictionaryToJson(
+    const chromeos::VariantDictionary& dictionary);
+
+std::unique_ptr<base::Value> AnyToJson(const chromeos::Any& value) {
+  if (value.IsTypeCompatible<chromeos::VariantDictionary>())
+    return DictionaryToJson(value.Get<chromeos::VariantDictionary>());
+
+  if (value.IsTypeCompatible<std::string>()) {
+    return std::unique_ptr<base::Value>{
+        new base::StringValue(value.Get<std::string>())};
+  }
+
+  if (value.IsTypeCompatible<double>()) {
+    return std::unique_ptr<base::Value>{
+        new base::FundamentalValue(value.Get<double>())};
+  }
+
+  if (value.IsTypeCompatible<bool>()) {
+    return std::unique_ptr<base::Value>{
+        new base::FundamentalValue(value.Get<bool>())};
+  }
+
+  if (value.IsTypeCompatible<int>()) {
+    return std::unique_ptr<base::Value>{
+        new base::FundamentalValue(value.Get<int>())};
+  }
+
+  LOG(FATAL) << "Unsupported type:" << value.GetType().name();
+  return {};
+}
+
+std::unique_ptr<base::DictionaryValue> DictionaryToJson(
+    const chromeos::VariantDictionary& dictionary) {
+  std::unique_ptr<base::DictionaryValue> result{new base::DictionaryValue};
+  for (const auto& it : dictionary)
+    result->Set(it.first, AnyToJson(it.second).release());
+  return result;
+}
+
+std::string DictionaryToString(const chromeos::VariantDictionary& dictionary) {
+  std::unique_ptr<base::DictionaryValue> json{DictionaryToJson(dictionary)};
+  std::string str;
+  base::JSONWriter::Write(json.get(), &str);
+  return str;
+}
+
 }  // anonymous namespace
 
 class Daemon : public chromeos::DBusDaemon {
@@ -67,8 +116,11 @@
                               const std::string& property_name) {
   printf("Notification: property '%s' on command '%s' changed.\n",
          property_name.c_str(), command->id().c_str());
-  printf("  Current command status: '%s' (%d)\n",
-         command->status().c_str(), command->progress());
+  printf("  Current command status: '%s'\n", command->status().c_str());
+  std::string progress = DictionaryToString(command->progress());
+  printf("  Current command progress: %s\n", progress.c_str());
+  std::string results = DictionaryToString(command->results());
+  printf("  Current command results: %s\n", results.c_str());
 }
 
 void Daemon::OnBuffetCommand(org::chromium::Buffet::CommandProxy* command) {
@@ -87,28 +139,25 @@
   printf("              ID: %s\n", command->id().c_str());
   printf("          status: %s\n", command->status().c_str());
   printf("          origin: %s\n", command->origin().c_str());
-  printf(" # of parameters: %" PRIuS "\n", command->parameters().size());
-  auto keys = chromeos::GetMapKeysAsVector(command->parameters());
-  std::string param_names = chromeos::string_utils::Join(", ", keys);
-  printf(" parameter names: %s\n", param_names.c_str());
+  std::string param_names = DictionaryToString(command->parameters());
+  printf(" parameters: %s\n", param_names.c_str());
   OnCommandProgress(command, 0);
 }
 
 void Daemon::OnCommandProgress(org::chromium::Buffet::CommandProxy* command,
                                int progress) {
+  printf("Updating command '%s' progress to %d%%\n", command->id().c_str(),
+         progress);
+  auto new_progress = command->progress();
+  new_progress["progress"] = progress;
+  command->SetProgress(new_progress, nullptr);
+
   if (progress >= 100) {
     command->Done(nullptr);
   } else {
-    printf("Updating command '%s' progress to %d%%\n",
-           command->id().c_str(), progress);
-    command->SetProgress(progress, nullptr);
-    progress += 10;
     base::MessageLoop::current()->PostDelayedTask(
-        FROM_HERE,
-        base::Bind(&Daemon::OnCommandProgress,
-                   base::Unretained(this),
-                   command,
-                   progress),
+        FROM_HERE, base::Bind(&Daemon::OnCommandProgress,
+                              base::Unretained(this), command, progress + 10),
         base::TimeDelta::FromSeconds(1));
   }
 }