Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 22c321c7 authored by Nandana Dutt's avatar Nandana Dutt Committed by Gerrit Code Review
Browse files

Merge "Refactor options parsing in dumpstate"

parents 43a22f7e 3f8c717b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -133,6 +133,7 @@ cc_test {
    name: "dumpstate_test",
    defaults: ["dumpstate_defaults"],
    srcs: [
        "dumpstate.cpp",
        "tests/dumpstate_test.cpp",
    ],
    static_libs: ["libgmock"],
+133 −119
Original line number Diff line number Diff line
@@ -1745,131 +1745,140 @@ static void Vibrate(int duration_ms) {
    // clang-format on
}

/** Main entry point for dumpstate. */
int run_main(int argc, char* argv[]) {
    int do_add_date = 0;
    int do_zip_file = 0;
    int do_vibrate = 1;
    char* use_outfile = nullptr;
    int use_socket = 0;
    int use_control_socket = 0;
    int do_fb = 0;
    int do_broadcast = 0;
    int is_remote_mode = 0;
    bool show_header_only = false;
    bool do_start_service = false;
    bool telephony_only = false;
    bool wifi_only = false;
    int dup_stdout_fd;
    int dup_stderr_fd;

    /* set as high priority, and protect from OOM killer */
    setpriority(PRIO_PROCESS, 0, -20);

    FILE* oom_adj = fopen("/proc/self/oom_score_adj", "we");
    if (oom_adj) {
        fputs("-1000", oom_adj);
        fclose(oom_adj);
    } else {
        /* fallback to kernels <= 2.6.35 */
        oom_adj = fopen("/proc/self/oom_adj", "we");
        if (oom_adj) {
            fputs("-17", oom_adj);
            fclose(oom_adj);
        }
    }

    /* parse arguments */
int Dumpstate::ParseCommandlineOptions(int argc, char* argv[]) {
    int ret = -1;  // success
    int c;
    while ((c = getopt(argc, argv, "dho:svqzpPBRSV:")) != -1) {
        switch (c) {
            // clang-format off
            case 'd': do_add_date = 1;            break;
            case 'z': do_zip_file = 1;            break;
            case 'o': use_outfile = optarg;       break;
            case 's': use_socket = 1;             break;
            case 'S': use_control_socket = 1;     break;
            case 'v': show_header_only = true;    break;
            case 'q': do_vibrate = 0;             break;
            case 'p': do_fb = 1;                  break;
            case 'P': ds.update_progress_ = true; break;
            case 'R': is_remote_mode = 1;         break;
            case 'B': do_broadcast = 1;           break;
            case 'd': options_.do_add_date = true;            break;
            case 'z': options_.do_zip_file = true;            break;
            case 'o': options_.use_outfile = optarg;          break;
            case 's': options_.use_socket = true;             break;
            case 'S': options_.use_control_socket = true;     break;
            case 'v': options_.show_header_only = true;       break;
            case 'q': options_.do_vibrate = false;            break;
            case 'p': options_.do_fb = true;                  break;
            case 'P': update_progress_ = true;                break;
            case 'R': options_.is_remote_mode = true;         break;
            case 'B': options_.do_broadcast = true;           break;
            case 'V':                                         break;  // compatibility no-op
            case 'h':
                ShowUsageAndExit(0);
                ret = 0;
                break;
            default:
                fprintf(stderr, "Invalid option: %c\n", c);
                ShowUsageAndExit();
                ret = 1;
                break;
                // clang-format on
        }
    }

    // TODO: use helper function to convert argv into a string
    for (int i = 0; i < argc; i++) {
        ds.args_ += argv[i];
        args_ += argv[i];
        if (i < argc - 1) {
            ds.args_ += " ";
            args_ += " ";
        }
    }

    ds.extra_options_ = android::base::GetProperty(PROPERTY_EXTRA_OPTIONS, "");
    if (!ds.extra_options_.empty()) {
    // Reset next index used by getopt so this can be called multiple times, for eg, in tests.
    optind = 1;
    return ret;
}

// TODO: Move away from system properties when we have binder.
void Dumpstate::SetOptionsFromProperties() {
    extra_options_ = android::base::GetProperty(PROPERTY_EXTRA_OPTIONS, "");
    if (!extra_options_.empty()) {
        // Framework uses a system property to override some command-line args.
        // Currently, it contains the type of the requested bugreport.
        if (ds.extra_options_ == "bugreportplus") {
        if (extra_options_ == "bugreportplus") {
            // Currently, the dumpstate binder is only used by Shell to update progress.
            do_start_service = true;
            ds.update_progress_ = true;
            do_fb = 0;
        } else if (ds.extra_options_ == "bugreportremote") {
            do_vibrate = 0;
            is_remote_mode = 1;
            do_fb = 0;
        } else if (ds.extra_options_ == "bugreportwear") {
            do_start_service = true;
            ds.update_progress_ = true;
            do_zip_file = 1;
        } else if (ds.extra_options_ == "bugreporttelephony") {
            telephony_only = true;
        } else if (ds.extra_options_ == "bugreportwifi") {
            wifi_only = true;
            do_zip_file = 1;
            options_.do_start_service = true;
            update_progress_ = true;
            options_.do_fb = false;
        } else if (extra_options_ == "bugreportremote") {
            options_.do_vibrate = false;
            options_.is_remote_mode = true;
            options_.do_fb = false;
        } else if (extra_options_ == "bugreportwear") {
            options_.do_start_service = true;
            update_progress_ = true;
            options_.do_zip_file = true;
        } else if (extra_options_ == "bugreporttelephony") {
            options_.telephony_only = true;
        } else if (extra_options_ == "bugreportwifi") {
            options_.wifi_only = true;
            options_.do_zip_file = true;
        } else {
            MYLOGE("Unknown extra option: %s\n", ds.extra_options_.c_str());
            MYLOGE("Unknown extra option: %s\n", extra_options_.c_str());
        }
        // Reset the property
        android::base::SetProperty(PROPERTY_EXTRA_OPTIONS, "");
    }

    ds.notification_title = android::base::GetProperty(PROPERTY_EXTRA_TITLE, "");
    if (!ds.notification_title.empty()) {
    notification_title = android::base::GetProperty(PROPERTY_EXTRA_TITLE, "");
    if (!notification_title.empty()) {
        // Reset the property
        android::base::SetProperty(PROPERTY_EXTRA_TITLE, "");

        ds.notification_description = android::base::GetProperty(PROPERTY_EXTRA_DESCRIPTION, "");
        if (!ds.notification_description.empty()) {
        notification_description = android::base::GetProperty(PROPERTY_EXTRA_DESCRIPTION, "");
        if (!notification_description.empty()) {
            // Reset the property
            android::base::SetProperty(PROPERTY_EXTRA_DESCRIPTION, "");
        }
        MYLOGD("notification (title:  %s, description: %s)\n",
               ds.notification_title.c_str(), ds.notification_description.c_str());
        MYLOGD("notification (title:  %s, description: %s)\n", notification_title.c_str(),
               notification_description.c_str());
    }
}

    if ((do_zip_file || do_add_date || ds.update_progress_ || do_broadcast) && !use_outfile) {
        ExitOnInvalidArgs();
bool Dumpstate::ValidateOptions() {
    if ((options_.do_zip_file || options_.do_add_date || ds.update_progress_ ||
         options_.do_broadcast) &&
        options_.use_outfile.empty()) {
        return false;
    }

    if (use_control_socket && !do_zip_file) {
        ExitOnInvalidArgs();
    if (options_.use_control_socket && !options_.do_zip_file) {
        return false;
    }

    if (ds.update_progress_ && !do_broadcast) {
        ExitOnInvalidArgs();
    if (ds.update_progress_ && !options_.do_broadcast) {
        return false;
    }

    if (options_.is_remote_mode && (ds.update_progress_ || !options_.do_broadcast ||
                                    !options_.do_zip_file || !options_.do_add_date)) {
        return false;
    }
    return true;
}

    if (is_remote_mode && (ds.update_progress_ || !do_broadcast || !do_zip_file || !do_add_date)) {
/* Main entry point for dumpstate. */
int run_main(int argc, char* argv[]) {
    /* set as high priority, and protect from OOM killer */
    setpriority(PRIO_PROCESS, 0, -20);

    FILE* oom_adj = fopen("/proc/self/oom_score_adj", "we");
    if (oom_adj) {
        fputs("-1000", oom_adj);
        fclose(oom_adj);
    } else {
        /* fallback to kernels <= 2.6.35 */
        oom_adj = fopen("/proc/self/oom_adj", "we");
        if (oom_adj) {
            fputs("-17", oom_adj);
            fclose(oom_adj);
        }
    }

    int status = ds.ParseCommandlineOptions(argc, argv);
    if (status != -1) {
        ShowUsageAndExit(status);
    }
    ds.SetOptionsFromProperties();
    if (!ds.ValidateOptions()) {
        ExitOnInvalidArgs();
    }

@@ -1884,17 +1893,20 @@ int run_main(int argc, char* argv[]) {
        exit(1);
    }

    if (show_header_only) {
    // TODO: make const reference, but first avoid setting do_zip_file below.
    Dumpstate::DumpOptions& options = ds.options_;
    if (options.show_header_only) {
        ds.PrintHeader();
        exit(0);
    }

    /* redirect output if needed */
    bool is_redirecting = !use_socket && use_outfile;
    // Redirect output if needed
    bool is_redirecting = !options.use_socket && !options.use_outfile.empty();

    // TODO: temporarily set progress until it's part of the Dumpstate constructor
    std::string stats_path =
        is_redirecting ? android::base::StringPrintf("%s/dumpstate-stats.txt", dirname(use_outfile))
    std::string stats_path = is_redirecting
                                 ? android::base::StringPrintf("%s/dumpstate-stats.txt",
                                                               dirname(options.use_outfile.c_str()))
                                 : "";
    ds.progress_.reset(new Progress(stats_path));

@@ -1907,7 +1919,7 @@ int run_main(int argc, char* argv[]) {

    register_sig_handler();

    if (do_start_service) {
    if (options.do_start_service) {
        MYLOGI("Starting 'dumpstate' service\n");
        android::status_t ret;
        if ((ret = android::os::DumpstateService::Start()) != android::OK) {
@@ -1928,23 +1940,24 @@ int run_main(int argc, char* argv[]) {

    // If we are going to use a socket, do it as early as possible
    // to avoid timeouts from bugreport.
    if (use_socket) {
    if (options.use_socket) {
        redirect_to_socket(stdout, "dumpstate");
    }

    if (use_control_socket) {
    if (options.use_control_socket) {
        MYLOGD("Opening control socket\n");
        ds.control_socket_fd_ = open_socket("dumpstate");
        ds.update_progress_ = 1;
    }

    if (is_redirecting) {
        ds.bugreport_dir_ = dirname(use_outfile);
        ds.bugreport_dir_ = dirname(options.use_outfile.c_str());
        std::string build_id = android::base::GetProperty("ro.build.id", "UNKNOWN_BUILD");
        std::string device_name = android::base::GetProperty("ro.product.name", "UNKNOWN_DEVICE");
        ds.base_name_ = android::base::StringPrintf("%s-%s-%s", basename(use_outfile),
        ds.base_name_ =
            android::base::StringPrintf("%s-%s-%s", basename(options.use_outfile.c_str()),
                                        device_name.c_str(), build_id.c_str());
        if (do_add_date) {
        if (options.do_add_date) {
            char date[80];
            strftime(date, sizeof(date), "%Y-%m-%d-%H-%M-%S", localtime(&ds.now_));
            ds.name_ = date;
@@ -1952,13 +1965,13 @@ int run_main(int argc, char* argv[]) {
            ds.name_ = "undated";
        }

        if (telephony_only) {
        if (options.telephony_only) {
            ds.base_name_ += "-telephony";
        } else if (wifi_only) {
        } else if (options.wifi_only) {
            ds.base_name_ += "-wifi";
        }

        if (do_fb) {
        if (options.do_fb) {
            ds.screenshot_path_ = ds.GetPath(".png");
        }
        ds.tmp_path_ = ds.GetPath(".tmp");
@@ -1974,14 +1987,14 @@ int run_main(int argc, char* argv[]) {
            ds.bugreport_dir_.c_str(), ds.base_name_.c_str(), ds.name_.c_str(),
            ds.log_path_.c_str(), ds.tmp_path_.c_str(), ds.screenshot_path_.c_str());

        if (do_zip_file) {
        if (options.do_zip_file) {
            ds.path_ = ds.GetPath(".zip");
            MYLOGD("Creating initial .zip file (%s)\n", ds.path_.c_str());
            create_parent_dirs(ds.path_.c_str());
            ds.zip_file.reset(fopen(ds.path_.c_str(), "wb"));
            if (ds.zip_file == nullptr) {
                MYLOGE("fopen(%s, 'wb'): %s\n", ds.path_.c_str(), strerror(errno));
                do_zip_file = 0;
                options.do_zip_file = false;
            } else {
                ds.zip_writer_.reset(new ZipWriter(ds.zip_file.get()));
            }
@@ -1989,7 +2002,7 @@ int run_main(int argc, char* argv[]) {
        }

        if (ds.update_progress_) {
            if (do_broadcast) {
            if (options.do_broadcast) {
                // clang-format off

                std::vector<std::string> am_args = {
@@ -2002,7 +2015,7 @@ int run_main(int argc, char* argv[]) {
                // clang-format on
                SendBroadcast("com.android.internal.intent.action.BUGREPORT_STARTED", am_args);
            }
            if (use_control_socket) {
            if (options.use_control_socket) {
                dprintf(ds.control_socket_fd_, "BEGIN:%s\n", ds.path_.c_str());
            }
        }
@@ -2015,11 +2028,11 @@ int run_main(int argc, char* argv[]) {
        fclose(cmdline);
    }

    if (do_vibrate) {
    if (options.do_vibrate) {
        Vibrate(150);
    }

    if (do_fb && ds.do_early_screenshot_) {
    if (options.do_fb && ds.do_early_screenshot_) {
        if (ds.screenshot_path_.empty()) {
            // should not have happened
            MYLOGE("INTERNAL ERROR: skipping early screenshot because path was not set\n");
@@ -2029,13 +2042,15 @@ int run_main(int argc, char* argv[]) {
        }
    }

    if (do_zip_file) {
    if (options.do_zip_file) {
        if (chown(ds.path_.c_str(), AID_SHELL, AID_SHELL)) {
            MYLOGE("Unable to change ownership of zip file %s: %s\n", ds.path_.c_str(),
                   strerror(errno));
        }
    }

    int dup_stdout_fd;
    int dup_stderr_fd;
    if (is_redirecting) {
        TEMP_FAILURE_RETRY(dup_stderr_fd = dup(fileno(stderr)));
        redirect_to_file(stderr, const_cast<char*>(ds.log_path_.c_str()));
@@ -2062,10 +2077,10 @@ int run_main(int argc, char* argv[]) {
    // duration is logged into MYLOG instead.
    ds.PrintHeader();

    if (telephony_only) {
    if (options.telephony_only) {
        DumpstateTelephonyOnly();
        ds.DumpstateBoard();
    } else if (wifi_only) {
    } else if (options.wifi_only) {
        DumpstateWifiOnly();
    } else {
        // Dumps systrace right away, otherwise it will be filled with unnecessary events.
@@ -2124,8 +2139,7 @@ int run_main(int argc, char* argv[]) {
    }

    /* rename or zip the (now complete) .tmp file to its final location */
    if (use_outfile) {

    if (!options.use_outfile.empty()) {
        /* check if user changed the suffix using system properties */
        std::string name = android::base::GetProperty(
            android::base::StringPrintf("dumpstate.%d.name", ds.pid_), "");
@@ -2154,7 +2168,7 @@ int run_main(int argc, char* argv[]) {
        }

        bool do_text_file = true;
        if (do_zip_file) {
        if (options.do_zip_file) {
            if (!ds.FinishZipFile()) {
                MYLOGE("Failed to finish zip file; sending text bugreport instead\n");
                do_text_file = true;
@@ -2183,7 +2197,7 @@ int run_main(int argc, char* argv[]) {
                ds.path_.clear();
            }
        }
        if (use_control_socket) {
        if (options.use_control_socket) {
            if (do_text_file) {
                dprintf(ds.control_socket_fd_,
                        "FAIL:could not create zip file, check %s "
@@ -2196,7 +2210,7 @@ int run_main(int argc, char* argv[]) {
    }

    /* vibrate a few but shortly times to let user know it's finished */
    if (do_vibrate) {
    if (options.do_vibrate) {
        for (int i = 0; i < 3; i++) {
            Vibrate(75);
            usleep((75 + 50) * 1000);
@@ -2204,7 +2218,7 @@ int run_main(int argc, char* argv[]) {
    }

    /* tell activity manager we're done */
    if (do_broadcast) {
    if (options.do_broadcast) {
        if (!ds.path_.empty()) {
            MYLOGI("Final bugreport path: %s\n", ds.path_.c_str());
            // clang-format off
@@ -2218,7 +2232,7 @@ int run_main(int argc, char* argv[]) {
                 "--es", "android.intent.extra.DUMPSTATE_LOG", ds.log_path_
            };
            // clang-format on
            if (do_fb) {
            if (options.do_fb) {
                am_args.push_back("--es");
                am_args.push_back("android.intent.extra.SCREENSHOT");
                am_args.push_back(ds.screenshot_path_);
@@ -2233,7 +2247,7 @@ int run_main(int argc, char* argv[]) {
                    am_args.push_back(ds.notification_description);
                }
            }
            if (is_remote_mode) {
            if (options.is_remote_mode) {
                am_args.push_back("--es");
                am_args.push_back("android.intent.extra.REMOTE_BUGREPORT_HASH");
                am_args.push_back(SHA256_file_hash(ds.path_));
@@ -2256,7 +2270,7 @@ int run_main(int argc, char* argv[]) {
        TEMP_FAILURE_RETRY(dup2(dup_stderr_fd, fileno(stderr)));
    }

    if (use_control_socket && ds.control_socket_fd_ != -1) {
    if (options.use_control_socket && ds.control_socket_fd_ != -1) {
        MYLOGD("Closing control socket\n");
        close(ds.control_socket_fd_);
    }
+38 −1
Original line number Diff line number Diff line
@@ -290,14 +290,51 @@ class Dumpstate {
    /* Returns true if the current version supports priority dump feature. */
    bool CurrentVersionSupportsPriorityDumps() const;

    // TODO: initialize fields on constructor
    // TODO: revisit the return values later.
    /*
     * Parses commandline arguments and sets runtime options accordingly.
     *
     * Returns 0 or positive number if the caller should exit with returned value as
     * exit code, or returns -1 if caller should proceed with execution.
     */
    int ParseCommandlineOptions(int argc, char* argv[]);

    /* Sets runtime options from the system properties. */
    void SetOptionsFromProperties();

    /* Returns true if the options set so far are consistent. */
    bool ValidateOptions();

    // TODO: add update_progress_ & other options from DumpState.
    /*
     * Structure to hold options that determine the behavior of dumpstate.
     */
    struct DumpOptions {
        bool do_add_date = false;
        bool do_zip_file = false;
        bool do_vibrate = true;
        bool use_socket = false;
        bool use_control_socket = false;
        bool do_fb = false;
        bool do_broadcast = false;
        bool is_remote_mode = false;
        bool show_header_only = false;
        bool do_start_service = false;
        bool telephony_only = false;
        bool wifi_only = false;
        std::string use_outfile;
    };

    // TODO: initialize fields on constructor
    // dumpstate id - unique after each device reboot.
    uint32_t id_;

    // dumpstate pid
    pid_t pid_;

    // Runtime options.
    DumpOptions options_;

    // Whether progress updates should be published.
    bool update_progress_ = false;

+154 −0
Original line number Diff line number Diff line
@@ -54,6 +54,8 @@ using ::testing::internal::CaptureStdout;
using ::testing::internal::GetCapturedStderr;
using ::testing::internal::GetCapturedStdout;

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

class DumpstateListenerMock : public IDumpstateListener {
  public:
    MOCK_METHOD1(onProgressUpdated, binder::Status(int32_t progress));
@@ -144,6 +146,7 @@ class DumpstateTest : public DumpstateBaseTest {
        ds.progress_.reset(new Progress());
        ds.update_progress_ = false;
        ds.update_progress_threshold_ = 0;
        ds.options_ = Dumpstate::DumpOptions();
    }

    // Runs a command and capture `stdout` and `stderr`.
@@ -201,6 +204,157 @@ class DumpstateTest : public DumpstateBaseTest {
    Dumpstate& ds = Dumpstate::GetInstance();
};

TEST_F(DumpstateTest, ParseCommandlineOptionsNone) {
    // clang-format off
    char* argv[] = {
        const_cast<char*>("dumpstate")
    };
    // clang-format on

    int ret = ds.ParseCommandlineOptions(ARRAY_SIZE(argv), argv);
    EXPECT_EQ(-1, ret);
    EXPECT_FALSE(ds.options_.do_add_date);
    EXPECT_FALSE(ds.options_.do_zip_file);
    EXPECT_EQ("", ds.options_.use_outfile);
    EXPECT_FALSE(ds.options_.use_socket);
    EXPECT_FALSE(ds.options_.use_control_socket);
    EXPECT_FALSE(ds.options_.show_header_only);
    EXPECT_TRUE(ds.options_.do_vibrate);
    EXPECT_FALSE(ds.options_.do_fb);
    EXPECT_FALSE(ds.update_progress_);
    EXPECT_FALSE(ds.options_.is_remote_mode);
    EXPECT_FALSE(ds.options_.do_broadcast);
}

TEST_F(DumpstateTest, ParseCommandlineOptionsPartial1) {
    // clang-format off
    char* argv[] = {
        const_cast<char*>("dumpstate"),
        const_cast<char*>("-d"),
        const_cast<char*>("-z"),
        const_cast<char*>("-o abc"),
        const_cast<char*>("-s"),
        const_cast<char*>("-S"),

    };
    // clang-format on
    int ret = ds.ParseCommandlineOptions(ARRAY_SIZE(argv), argv);
    EXPECT_EQ(-1, ret);
    EXPECT_TRUE(ds.options_.do_add_date);
    EXPECT_TRUE(ds.options_.do_zip_file);
    // TODO: Maybe we should trim the filename
    EXPECT_EQ(" abc", std::string(ds.options_.use_outfile));
    EXPECT_TRUE(ds.options_.use_socket);
    EXPECT_TRUE(ds.options_.use_control_socket);

    // Other options retain default values
    EXPECT_FALSE(ds.options_.show_header_only);
    EXPECT_TRUE(ds.options_.do_vibrate);
    EXPECT_FALSE(ds.options_.do_fb);
    EXPECT_FALSE(ds.update_progress_);
    EXPECT_FALSE(ds.options_.is_remote_mode);
    EXPECT_FALSE(ds.options_.do_broadcast);
}

TEST_F(DumpstateTest, ParseCommandlineOptionsPartial2) {
    // clang-format off
    char* argv[] = {
        const_cast<char*>("dumpstate"),
        const_cast<char*>("-v"),
        const_cast<char*>("-q"),
        const_cast<char*>("-p"),
        const_cast<char*>("-P"),
        const_cast<char*>("-R"),
        const_cast<char*>("-B"),
    };
    // clang-format on
    int ret = ds.ParseCommandlineOptions(ARRAY_SIZE(argv), argv);
    EXPECT_EQ(-1, ret);
    EXPECT_TRUE(ds.options_.show_header_only);
    EXPECT_FALSE(ds.options_.do_vibrate);
    EXPECT_TRUE(ds.options_.do_fb);
    EXPECT_TRUE(ds.update_progress_);
    EXPECT_TRUE(ds.options_.is_remote_mode);
    EXPECT_TRUE(ds.options_.do_broadcast);

    // Other options retain default values
    EXPECT_FALSE(ds.options_.do_add_date);
    EXPECT_FALSE(ds.options_.do_zip_file);
    EXPECT_EQ("", ds.options_.use_outfile);
    EXPECT_FALSE(ds.options_.use_socket);
    EXPECT_FALSE(ds.options_.use_control_socket);
}

TEST_F(DumpstateTest, ParseCommandlineOptionsHelp) {
    // clang-format off
    char* argv[] = {
        const_cast<char*>("dumpstate"),
        const_cast<char*>("-h")
    };
    // clang-format on
    int ret = ds.ParseCommandlineOptions(ARRAY_SIZE(argv), argv);

    // -h is for help. Caller exit with code = 0 after printing usage, so expect return = 0.
    EXPECT_EQ(0, ret);
}

TEST_F(DumpstateTest, ParseCommandlineOptionsUnknown) {
    // clang-format off
    char* argv[] = {
        const_cast<char*>("dumpstate"),
        const_cast<char*>("-u")  // unknown flag
    };
    // clang-format on
    int ret = ds.ParseCommandlineOptions(ARRAY_SIZE(argv), argv);

    // -u is unknown. Caller exit with code = 1 to show execution failure, after printing usage,
    // so expect return = 1.
    EXPECT_EQ(1, ret);
}

TEST_F(DumpstateTest, ValidateOptionsNeedOutfile1) {
    ds.options_.do_zip_file = true;
    EXPECT_FALSE(ds.ValidateOptions());
    ds.options_.use_outfile = "a/b/c";
    EXPECT_TRUE(ds.ValidateOptions());
}

TEST_F(DumpstateTest, ValidateOptionsNeedOutfile2) {
    ds.options_.do_broadcast = true;
    EXPECT_FALSE(ds.ValidateOptions());
    ds.options_.use_outfile = "a/b/c";
    EXPECT_TRUE(ds.ValidateOptions());
}

TEST_F(DumpstateTest, ValidateOptionsNeedZipfile) {
    ds.options_.use_control_socket = true;
    EXPECT_FALSE(ds.ValidateOptions());

    ds.options_.do_zip_file = true;
    ds.options_.use_outfile = "a/b/c";  // do_zip_file needs outfile
    EXPECT_TRUE(ds.ValidateOptions());
}

TEST_F(DumpstateTest, ValidateOptionsUpdateProgressNeedsBroadcast) {
    ds.update_progress_ = true;
    ds.options_.use_outfile = "a/b/c";  // update_progress_ needs outfile
    EXPECT_FALSE(ds.ValidateOptions());

    ds.options_.do_broadcast = true;
    EXPECT_TRUE(ds.ValidateOptions());
}

TEST_F(DumpstateTest, ValidateOptionsRemoteMode) {
    ds.options_.is_remote_mode = true;
    EXPECT_FALSE(ds.ValidateOptions());

    ds.options_.do_broadcast = true;
    ds.options_.do_zip_file = true;
    ds.options_.do_add_date = true;
    ds.options_.use_outfile = "a/b/c";  // do_broadcast needs outfile
    EXPECT_TRUE(ds.ValidateOptions());
}

TEST_F(DumpstateTest, RunCommandNoArgs) {
    EXPECT_EQ(-1, RunCommand("", {}));
}