chromium/chrome/browser/ash/printing/cups_print_job_manager_utils_unittest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/printing/cups_print_job_manager_utils.h"

#include <memory>
#include <ostream>
#include <utility>
#include <vector>

#include "base/test/scoped_mock_clock_override.h"
#include "chrome/browser/ash/printing/cups_print_job.h"
#include "chrome/browser/ash/printing/history/print_job_info.pb.h"
#include "chrome/browser/printing/printer_query.h"
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/test/browser_task_environment.h"
#include "printing/backend/cups_jobs.h"
#include "printing/print_settings.h"
#include "printing/printed_document.h"
#include "printing/printer_status.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace {

using State = CupsPrintJob::State;
using ::printing::CupsJob;
using PrinterReason = ::printing::PrinterStatus::PrinterReason;
using ::chromeos::PrinterErrorCode;

constexpr int kTotalPages = 2;

// Timeout value defined in cups_print_job_manager_utils.cc
constexpr int kTimeout = 30;

struct Params {
  Params(State state,
         int pages,
         CupsJob::JobState job_state,
         std::vector<CupsJob::JobStateReason> job_state_reasons,
         int job_pages,
         State expected_state,
         int expected_pages)
      : state(state),
        pages(pages),
        job_state(job_state),
        job_state_reasons(std::move(job_state_reasons)),
        job_pages(job_pages),
        expected_state(expected_state),
        expected_pages(expected_pages) {}

  // Test state transition that does not affect the page count.
  // `job_pages` is set to `kTotalPages` so that terminal events such as
  // `JobState::COMPLETED` work properly.
  Params(State state,
         CupsJob::JobState job_state,
         std::vector<CupsJob::JobStateReason> job_state_reasons,
         State expected_state)
      : Params(state,
               0,
               job_state,
               std::move(job_state_reasons),
               kTotalPages,
               expected_state,
               0) {}

  State state;
  int pages;
  CupsJob::JobState job_state;
  std::vector<CupsJob::JobStateReason> job_state_reasons;
  // Current printed page count.
  // The total print job page count for the tests is `kTotalPages`.
  int job_pages;
  State expected_state;
  int expected_pages;
};

std::ostream& operator<<(std::ostream& out, const Params& params) {
  out << "state: " << static_cast<int>(params.state) << ", ";
  out << "pages: " << params.pages << ", ";
  out << "job_state: " << params.job_state << ", ";
  for (const auto& job_state_reason : params.job_state_reasons) {
    out << "job_state_reason: "
        << ::printing::ToJobStateReasonString(job_state_reason) << ", ";
  }
  out << "job_pages: " << params.job_pages << ", ";
  out << "expected_state: " << static_cast<int>(params.expected_state) << ", ";
  out << "expected_pages: " << params.expected_pages << std::endl;
  return out;
}

std::vector<Params> GenerateParams(CupsJob::JobState job_state,
                                   State terminal_state) {
  std::vector<Params> params;
  for (State state :
       {State::STATE_WAITING, State::STATE_STARTED, State::STATE_PAGE_DONE}) {
    params.emplace_back(state, job_state,
                        std::vector<CupsJob::JobStateReason>(), terminal_state);
  }
  return params;
}

// Transition from 'STATE_WAITING' to `STATE_STARTED` before
// `STATE_PAGE_DONE`, even if a page has finished printing.
std::vector<Params> WaitingToStarted() {
  std::vector<Params> params;

  for (int i = 0; i <= kTotalPages; i++) {
    params.emplace_back(State::STATE_WAITING, 0, CupsJob::PROCESSING,
                        std::vector<CupsJob::JobStateReason>(), i,
                        State::STATE_STARTED, i);
  }

  return params;
}

// Transition from `STATE_STARTED` to `STATE_PAGE_DOWN`.
std::vector<Params> StartedToPageDone() {
  std::vector<Params> params;

  for (int i = 1; i <= kTotalPages; i++) {
    params.emplace_back(State::STATE_STARTED, 1, CupsJob::PROCESSING,
                        std::vector<CupsJob::JobStateReason>(), i,
                        State::STATE_PAGE_DONE, i);
  }

  return params;
}

// Process the second page and subsequent pages in `STATE_PAGE_DONE`.
std::vector<Params> PageDoneToPageDone() {
  std::vector<Params> params;

  for (int i = 1; i <= kTotalPages; i++) {
    for (int j = i; j <= kTotalPages; j++) {
      params.emplace_back(State::STATE_PAGE_DONE, i, CupsJob::PROCESSING,
                          std::vector<CupsJob::JobStateReason>(), j,
                          State::STATE_PAGE_DONE, j);
    }
  }

  return params;
}

using CupsPrintJobManagerUtilsTest = testing::TestWithParam<Params>;

INSTANTIATE_TEST_SUITE_P(
    Completed,
    CupsPrintJobManagerUtilsTest,
    testing::ValuesIn(GenerateParams(CupsJob::COMPLETED,
                                     State::STATE_DOCUMENT_DONE)));
INSTANTIATE_TEST_SUITE_P(
    Cancelled,
    CupsPrintJobManagerUtilsTest,
    testing::ValuesIn(GenerateParams(CupsJob::CANCELED,
                                     State::STATE_CANCELLED)));
INSTANTIATE_TEST_SUITE_P(
    Failed,
    CupsPrintJobManagerUtilsTest,
    testing::ValuesIn(GenerateParams(CupsJob::ABORTED, State::STATE_FAILED)));

INSTANTIATE_TEST_SUITE_P(
    ClientUnauthorizedToStateFailed,
    CupsPrintJobManagerUtilsTest,
    testing::Values(
        Params(State::STATE_SUSPENDED,
               CupsJob::HELD,
               std::vector<CupsJob::JobStateReason>{
                   CupsJob::JobStateReason::kCupsHeldForAuthentication},
               State::STATE_FAILED)));

INSTANTIATE_TEST_SUITE_P(
    FilterFailedtoStateFailed,
    CupsPrintJobManagerUtilsTest,
    testing::Values(
        Params(State::STATE_SUSPENDED,
               CupsJob::STOPPED,
               std::vector<CupsJob::JobStateReason>{
                   CupsJob::JobStateReason::kJobCompletedWithErrors},
               State::STATE_FAILED)));

INSTANTIATE_TEST_SUITE_P(
    Pending,
    CupsPrintJobManagerUtilsTest,
    testing::Values(Params(State::STATE_WAITING,
                           CupsJob::PENDING,
                           std::vector<CupsJob::JobStateReason>(),
                           State::STATE_WAITING)));

INSTANTIATE_TEST_SUITE_P(ProcessingWaitingToStarted,
                         CupsPrintJobManagerUtilsTest,
                         testing::ValuesIn(WaitingToStarted()));

INSTANTIATE_TEST_SUITE_P(
    ProcessingWaitingToStartedUnknownState,
    CupsPrintJobManagerUtilsTest,
    testing::Values(Params(State::STATE_WAITING,
                           0,
                           CupsJob::PROCESSING,
                           std::vector<CupsJob::JobStateReason>(),
                           -1,
                           State::STATE_STARTED,
                           0)));

INSTANTIATE_TEST_SUITE_P(
    ProcessingStartedToStarted,
    CupsPrintJobManagerUtilsTest,
    testing::Values(Params(State::STATE_STARTED,
                           0,
                           CupsJob::PROCESSING,
                           std::vector<CupsJob::JobStateReason>(),
                           0,
                           State::STATE_STARTED,
                           0)));

INSTANTIATE_TEST_SUITE_P(
    ProcessingStartedToStartedUnknownState,
    CupsPrintJobManagerUtilsTest,
    testing::Values(Params(State::STATE_STARTED,
                           0,
                           CupsJob::PROCESSING,
                           std::vector<CupsJob::JobStateReason>(),
                           -1,
                           State::STATE_STARTED,
                           0)));

INSTANTIATE_TEST_SUITE_P(ProcessingStartedToPageDone,
                         CupsPrintJobManagerUtilsTest,
                         testing::ValuesIn(StartedToPageDone()));

INSTANTIATE_TEST_SUITE_P(ProcessingPageDoneToPageDone,
                         CupsPrintJobManagerUtilsTest,
                         testing::ValuesIn(PageDoneToPageDone()));

TEST_P(CupsPrintJobManagerUtilsTest, UpdatePrintJob) {
  const Params& params = GetParam();
  CupsJob job;
  job.id = 0;
  job.state = params.job_state;
  job.current_pages = params.job_pages;
  for (const auto& job_state_reason : params.job_state_reasons) {
    job.state_reasons.push_back(
        ::printing::ToJobStateReasonString(job_state_reason).data());
  }
  CupsPrintJob print_job(chromeos::Printer(), 0, std::string(), kTotalPages,
                         crosapi::mojom::PrintJob::Source::kUnknown,
                         std::string(), printing::proto::PrintSettings());
  print_job.set_state(params.state);
  print_job.set_printed_page_number(params.pages);
  bool expected_print_job_updated = params.state != params.expected_state ||
                                    params.pages != params.expected_pages;
  EXPECT_EQ(expected_print_job_updated,
            UpdatePrintJob(::printing::PrinterStatus(), job, &print_job));
  EXPECT_EQ(params.expected_state, print_job.state());
  EXPECT_EQ(params.expected_pages, print_job.printed_page_number());
}

// Testing the behavior that CUPS is allowed to have a few seconds to reset a
// connection ot the printer.
TEST(CupsPrintJobManagerUtilsTest, UpdatePrintJobTimeout) {
  base::ScopedMockClockOverride mock_clock;

  CupsJob job;
  job.id = 0;
  job.state = CupsJob::PROCESSING;

  CupsPrintJob print_job(chromeos::Printer(), 0, std::string(), kTotalPages,
                         crosapi::mojom::PrintJob::Source::kUnknown,
                         std::string(), printing::proto::PrintSettings());
  print_job.set_state(State::STATE_STARTED);

  PrinterReason printer_reason;
  printer_reason.severity = PrinterReason::Severity::kError;
  printer_reason.reason = PrinterReason::Reason::kTimedOut;

  ::printing::PrinterStatus printer_status;
  printer_status.reasons.push_back(printer_reason);

  // Idle time is less than CUPS timeout limit. No error should be found.
  mock_clock.Advance(base::Seconds(kTimeout - 1));

  UpdatePrintJob(printer_status, job, &print_job);

  EXPECT_EQ(print_job.error_code(), PrinterErrorCode::NO_ERROR);
  EXPECT_EQ(print_job.state(), State::STATE_STARTED);

  // Idle time is more than CUPS timeout limit. Error should be returned.
  mock_clock.Advance(base::Seconds(kTimeout + 1));

  UpdatePrintJob(printer_status, job, &print_job);

  EXPECT_EQ(print_job.error_code(), PrinterErrorCode::PRINTER_UNREACHABLE);
  EXPECT_EQ(print_job.state(), CupsPrintJob::State::STATE_FAILED);
}

// Parameters required for CalculatePrintJobPageTotalTest.
struct PrintJobPageTotalParams {
  // Pages in document.
  int pages = 0;
  // Copies requested in settings.
  int copies = 0;
  // Expected total pages to be returned.
  int expected_total = 0;
};

// Test configuration required for setting up fake PrintedDocument.
class CalculatePrintJobPageTotalTest
    : public testing::TestWithParam<PrintJobPageTotalParams> {
 public:
  CalculatePrintJobPageTotalTest()
      : task_environment_(std::make_unique<content::BrowserTaskEnvironment>()) {
  }
  ~CalculatePrintJobPageTotalTest() override = default;

 private:
  // We need to create a MessageLoop, otherwise a bunch of things fails.
  std::unique_ptr<content::BrowserTaskEnvironment> task_environment_;
};

INSTANTIATE_TEST_SUITE_P(NoCopies,
                         CalculatePrintJobPageTotalTest,
                         testing::Values(PrintJobPageTotalParams{
                             .pages = 1,
                             .copies = 0,
                             .expected_total = 1}));

INSTANTIATE_TEST_SUITE_P(WithCopies,
                         CalculatePrintJobPageTotalTest,
                         testing::Values(PrintJobPageTotalParams{
                             .pages = 2,
                             .copies = 5,
                             .expected_total = 10}));

// Verify correct total number of pages generated based on incoming
// `::printing::PrintedDocument`.
TEST_P(CalculatePrintJobPageTotalTest, CalculatesPrintJobPageTotal) {
  const PrintJobPageTotalParams& param = GetParam();
  auto query =
      ::printing::PrinterQuery::Create(content::GlobalRenderFrameHostId());
  auto settings = std::make_unique<::printing::PrintSettings>();
  settings->set_copies(param.copies);
  auto new_doc = base::MakeRefCounted<::printing::PrintedDocument>(
      std::move(settings), u"fake.pdf", query->cookie());
  new_doc->set_page_count(param.pages);

  // Get total pages for document.
  int total_pages = CalculatePrintJobTotalPages(new_doc.get());

  // Verify totals match.
  ASSERT_EQ(param.expected_total, total_pages);
}

}  // namespace
}  // namespace ash