blob: 7da6951a4901a2ee84fdc5147d21643a800bec99 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package printer
import (
"bytes"
"compress/gzip"
"context"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"sync"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: TestPPDs,
Desc: "Verifies the PPD files pass cupstestppd and foomatic-rip",
Contacts: []string{
"batrapranav@chromium.org",
"cros-printing-dev@chromium.org",
},
Attr: []string{
"group:mainline",
"informational",
"group:paper-io",
"paper-io_printing",
},
// This may take a while on zork boards.
Timeout: time.Minute * 5,
SoftwareDeps: []string{"cros_internal", "cups"},
Data: []string{ppdsAll},
})
}
// Files to skip that currently fail the test.
var skip = map[string]bool{
"epson-20200615-EPSON_USB1.1_MFP_Full-Speed.ppd.gz": true,
"epson-20200615-EPSON_USB2.0_MFP_Hi-Speed.ppd.gz": true,
"epson-20200615-EPSON_USB2.0_Printer_Hi-speed.ppd.gz": true,
"foomatic-20200219-Apple-Color_StyleWriter_1500-lpstyl.ppd.gz": true,
"foomatic-20200219-Apple-Color_StyleWriter_2200-lpstyl.ppd.gz": true,
"foomatic-20200219-Apple-Color_StyleWriter_2400-lpstyl.ppd.gz": true,
"foomatic-20200219-Apple-Color_StyleWriter_2500-lpstyl.ppd.gz": true,
"foomatic-20200219-Apple-StyleWriter_1200-lpstyl.ppd.gz": true,
"foomatic-20200219-Apple-StyleWriter_I-lpstyl.ppd.gz": true,
"foomatic-20200219-Apple-StyleWriter_II-lpstyl.ppd.gz": true,
"foomatic-20200219-Brother-HL-1020-hl7x0.ppd.gz": true,
"foomatic-20200219-Brother-HL-720-hl7x0.ppd.gz": true,
"foomatic-20200219-Brother-HL-730-hl7x0.ppd.gz": true,
"foomatic-20200219-Brother-HL-820-hl7x0.ppd.gz": true,
"foomatic-20200219-Brother-MFC-9050-hl7x0.ppd.gz": true,
"foomatic-20200219-Compaq-IJ1200-drv_z42.ppd.gz": true,
"foomatic-20200219-Dell-3010cn-pxldpl.ppd.gz": true,
"foomatic-20200219-Generic-PCL_3_Printer-pcl3.ppd.gz": true,
"foomatic-20200219-HP-DesignJet_1050C-Postscript-HP.ppd.gz": true,
"foomatic-20200219-HP-DesignJet_1055CM-Postscript-HP.ppd.gz": true,
"foomatic-20200219-HP-DesignJet_2500CP-Postscript-HP.ppd.gz": true,
"foomatic-20200219-HP-DesignJet_3500CP-Postscript-HP.ppd.gz": true,
"foomatic-20200219-HP-DesignJet_5000PS-Postscript-HP.ppd.gz": true,
"foomatic-20200219-HP-DesignJet_5500ps-Postscript-HP.ppd.gz": true,
"foomatic-20200219-HP-DesignJet_800PS-Postscript-HP.ppd.gz": true,
"foomatic-20200219-HP-DeskJet_1000C-pnm2ppa.ppd.gz": true,
"foomatic-20200219-HP-DeskJet_712C-pnm2ppa.ppd.gz": true,
"foomatic-20200219-HP-DeskJet_722C-pnm2ppa.ppd.gz": true,
"foomatic-20200219-HP-DeskJet_820C-pnm2ppa.ppd.gz": true,
"foomatic-20200219-Imagistics-im8530-Postscript-Oce.ppd.gz": true,
"foomatic-20200219-KONICA_MINOLTA-bizhub_750-Postscript-KONICA_MINOLTA.ppd.gz": true,
"foomatic-20200219-Kyocera-CS-1815-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-CS-C2525E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-CS-C3225E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-CS-C3232E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-CS-C4035E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1000-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1000plus-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1010-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1018MFP-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1020D-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1030D-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1050-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1200-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1700-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1700plus-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1800-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1800plus-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1900-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-1920-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-3750-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-3800-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-3820N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-3830N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-3900DN-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-4000DN-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-5900C-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-6020-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-6026-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-6700-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-6900-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-7000-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-8000C-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-9000-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-9100DN-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-9120DN-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-9500DN-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-9520DN-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C5015N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C5016N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C5020N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C5025N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C5030N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C8008N-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C8100DN-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-FS-C8100DNplus_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-1815-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-2030-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-2530-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-3035-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-3530-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-4030-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-4035-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-5035-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-6030-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-8030-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-C2520-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-C2525E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-C3225-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-C3225E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-C3232-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-C3232E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Kyocera-KM-C4035E_KPDL-Postscript-Kyocera.ppd.gz": true,
"foomatic-20200219-Lexmark-X125-drv_x125.ppd.gz": true,
"foomatic-20200219-Lexmark-X73-drv_z42.ppd.gz": true,
"foomatic-20200219-Lexmark-Z42-drv_z42.ppd.gz": true,
"foomatic-20200219-Lexmark-Z43-drv_z42.ppd.gz": true,
"foomatic-20200219-Oki-B4300-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C5300-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C7100-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C7200-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C7300-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C7400-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C7500-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C9200-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C9300-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C9400-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Oki-C9500-Postscript-Oki.ppd.gz": true,
"foomatic-20200219-Samsung-C268x-Postscript-Samsung.ppd.gz": true,
"foomatic-20200219-Samsung-X3220-Postscript-Samsung.ppd.gz": true,
"foomatic-20200219-Samsung-X401-Postscript-Samsung.ppd.gz": true,
"foomatic-20200219-Samsung-X4300-Postscript-Samsung.ppd.gz": true,
"foomatic-20200219-Samsung-X703-Postscript-Samsung.ppd.gz": true,
"foomatic-20200219-Samsung-X7600-Postscript-Samsung.ppd.gz": true,
"hp-20190918-hplip-3.19.6-hp-Mimas17.ppd.gz": true,
"hp-20190918-hplip-3.19.6-hp-P15_CISS.ppd.gz": true,
"hp-20190918-hplip-3.19.6-hp-SPDOfficejetProBsize.ppd.gz": true,
"hplip-20200303-hplip-3.19.12-hp-designjet_Z6_24in-ps.ppd.gz": true,
"hplip-20200303-hplip-3.19.12-hp-designjet_Z6_44in-ps.ppd.gz": true,
"hplip-20200303-hplip-3.19.12-hp-designjet_Z6dr_44in-ps.ppd.gz": true,
"hplip-20201209-hplip-3.20.11-hp-P15_CISS.ppd.gz": true,
"hplip-20201209-hplip-3.20.11-hp-designjet_Z9_24in-ps.ppd.gz": true,
"hplip-20201209-hplip-3.20.11-hp-designjet_Z9_44in-ps.ppd.gz": true,
"hplip-20201209-hplip-3.20.11-hp-designjet_Z9dr_44in-ps.ppd.gz": true,
"hplip-20201209-hplip-3.20.11-hp-designjet_z2600_postscript-ps.ppd.gz": true,
"hplip-20201209-hplip-3.20.11-hp-designjet_z5400-postscript.ppd.gz": true,
"hplip-20201209-hplip-3.20.11-hp-designjet_z5600_postscript-ps.ppd.gz": true,
"konica_minolta-20200331-konica-minolta-20200331-konica-minolta-bizhub-758-jp-eu.ppd.gz": true,
"konica_minolta-20200331-konica-minolta-20200331-konica-minolta-bizhub-808-us.ppd.gz": true,
"kyocera-20190830-Kyocera_Generic_Monochrome.ppd.gz": true,
"star-20191209-mcp20.ppd.gz": true,
"star-20191209-mcp21.ppd.gz": true,
"star-20191209-mcp30.ppd.gz": true,
"star-20191209-mcp31.ppd.gz": true,
"star-20191209-pop10.ppd.gz": true,
"star-20191209-tsp651.ppd.gz": true,
}
// The PPD archive is generated via "go run ppdTool.go download" in
// src/third_party/autotest/files/client/site_tests/platform_PrinterPpds.
const ppdsAll = "ppds_all.tar.xz"
// fooRip detects if the PPD file uses the foomatic-rip print filter.
var fooRip = regexp.MustCompile(`foomatic-rip"`)
// fooCmd is the actual shell command (garbled) that foomatic filter executes.
// Two PPD files with the same fooCmd will tend to execute the same shell commands
// to generate print data. Note that setting the environment variable
// FOOMATIC_VERIFY_MODE prevents shell commands from being executed by the
// platform2 foomatic shell while still verifying that they are valid.
var fooCmd = regexp.MustCompile(`(?m)^\*FoomaticRIPCommandLine: "[^"]*"`)
type fileError struct {
file string
err error
}
// testPPD creates temp file ppdFile with contents ppdContents and
// tests it for validity. ppdFile is deleted before returning.
func testPPD(ctx context.Context, fooCache *sync.Map, ppdFile string, ppdContents []byte) error {
const pdf = `%PDF-1.0
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000052 00000 n
0000000101 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
147
%EOF`
if err := ioutil.WriteFile(ppdFile, ppdContents, 0644); err != nil {
return errors.Wrap(err, "failed to write file")
}
defer os.Remove(ppdFile)
// cupstestppd verifies that the PPD file is valid.
// -W translations ignores translations strings as these are not used by the
// ChromeOS version of CUPS. Debugd also calls cupstestppd with this flag.
cmd := testexec.CommandContext(ctx, "cupstestppd", "-W", "translations", ppdFile)
if err := cmd.Run(testexec.DumpLogOnError); err != nil {
return errors.Wrap(err, "cupstestppd")
}
// Check if the PPD file uses the foomatic-rip filter.
if !fooRip.Match(ppdContents) {
return nil
}
cmds := fooCmd.FindAll(ppdContents, 2)
if len(cmds) > 1 {
return errors.New("multiple FoomaticRIPCommandLine matches")
}
id := ""
if len(cmds) == 1 {
id = string(cmds[0])
}
if fooCache != nil {
val, ok := fooCache.Load(id)
if ok {
if val != "" {
return errors.Errorf("foomatic-rip: same error as %q", val)
}
return nil
}
}
cmd = testexec.CommandContext(ctx, "foomatic-rip", "1" /*jobID*/, "chronos" /*user*/, "Untitled" /*title*/, "1" /*copies*/, "" /*options*/)
cmd.Env = []string{"FOOMATIC_VERIFY_MODE=true",
"PATH=/bin:/usr/bin:/usr/libexec/cups/filter",
"PPD=" + ppdFile}
cmd.Stdin = bytes.NewReader([]byte(pdf))
err := cmd.Run(testexec.DumpLogOnError)
if fooCache != nil {
val := ""
if err != nil {
val = filepath.Base(ppdFile)
}
fooCache.Store(id, val)
}
if err != nil {
return errors.Wrap(err, "foomatic-rip")
}
return nil
}
// extractPPD extracts a gzip compressed PPD file and returns its contents.
func extractPPD(ctx context.Context, ppd []byte) ([]byte, error) {
buf, err := gzip.NewReader(bytes.NewReader(ppd))
if err != nil {
return nil, errors.Wrap(err, "failed to create reader")
}
ppd, readErr := ioutil.ReadAll(buf)
if err := buf.Close(); err != nil {
testing.ContextLog(ctx, "Failed to close gzip: ", err)
}
if readErr != nil {
return nil, errors.Wrap(readErr, "failed to read gzip")
}
return ppd, nil
}
// testPPDs receives PPD files to test (located in dir) through the
// files channel and returns any errors through the errors channel.
func testPPDs(ctx context.Context, dir string, files <-chan string, errors chan<- []fileError, fooCache *sync.Map) {
const gzExt = ".gz"
var errs []fileError
for file := range files {
if err := func() error {
ppd, err := ioutil.ReadFile(filepath.Join(dir, file))
if err != nil {
return err
}
if filepath.Ext(file) == gzExt {
ppd, err = extractPPD(ctx, ppd)
if err != nil {
return err
}
file = file[:len(file)-len(gzExt)]
}
return testPPD(ctx, fooCache, filepath.Join(dir, file), ppd)
}(); err != nil {
errs = append(errs, fileError{file, err})
}
}
errors <- errs
}
// extractArchive extracts a tar.xz archive via the tar command.
func extractArchive(ctx context.Context, src, dst string) error {
return testexec.CommandContext(ctx, "tar", "-xC", dst, "-f", src, "--strip-components=1").Run(testexec.DumpLogOnError)
}
func TestPPDs(ctx context.Context, s *testing.State) {
dir, err := ioutil.TempDir("", "")
if err != nil {
s.Fatal("Failed to create temp dir: ", err)
}
defer os.RemoveAll(dir)
// ppds_all.tar.xz takes around 60M when decompressed.
if err := extractArchive(ctx, s.DataPath(ppdsAll), dir); err != nil {
s.Fatal("Failed to extract archive: ", err)
}
files, err := ioutil.ReadDir(dir)
if err != nil {
s.Fatal("Failed to read directory: ", err)
}
n := len(files)
// Stride must be a power of 2.
// It is used to prevent fooCache cache misses by spacing out the
// lexicographically sorted files so that they are no longer in
// lexicographical order.
const stride = 1 << 6
if n <= stride+1 {
s.Fatalf("Too few files: %d found", n)
}
input := make(chan string, n)
// Always use at least 2 goroutines in case one of them is blocked.
numRoutines := runtime.NumCPU() + 1
testing.ContextLogf(ctx, "Creating %d goroutines", numRoutines)
errors := make(chan []fileError, numRoutines)
// fooCache maps fooCmd to an empty string if foomatic-rip succeeds and if it fails,
// the name of the PPD file on which it failed.
var fooCache *sync.Map
// Disabling cacheFoo roughly triples the runtime.
const cacheFoo = true
if cacheFoo {
fooCache = new(sync.Map)
}
for i := 0; i < numRoutines; i++ {
go testPPDs(ctx, dir, input, errors, fooCache)
}
// Ensure n is coprime to stride so that each file is selected exactly once.
if n&1 == 0 {
n--
input <- files[n].Name()
}
// Space the files out to avoid fooCache cache misses.
for i := 0; i != n; i += stride {
if i > n {
i -= n
}
if _, exists := skip[files[i].Name()]; !exists {
input <- files[i].Name()
}
}
close(input)
var errs []fileError
for i := 0; i < numRoutines; i++ {
errs = append(errs, <-errors...)
}
sort.Slice(errs, func(i, j int) bool { return errs[i].file < errs[j].file })
for _, fileErr := range errs {
s.Errorf("%s: %v", fileErr.file, fileErr.err)
}
}