blob: d139d706a2f58237a1905afea54fb264738a1151 [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 runtests
import (
"encoding/json"
"fmt"
"io"
"os"
"path"
"path/filepath"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
// DataSinkCopier copies data sinks from a remote host after a runtests invocation.
type DataSinkCopier struct {
viewer remoteViewer
}
// NewDataSinkCopier constructs a copier using the specified ssh client.
func NewDataSinkCopier(client *ssh.Client) (*DataSinkCopier, error) {
sftpClient, err := sftp.NewClient(client)
if err != nil {
return nil, err
}
viewer := &sftpViewer{sftpClient}
return &DataSinkCopier{viewer: viewer}, nil
}
// Copy copies data sinks using the copier's remote viewer.
func (c DataSinkCopier) Copy(remoteDir, localDir string) (DataSinkMap, error) {
return copyDataSinks(c.viewer, remoteDir, localDir)
}
func (c DataSinkCopier) Close() error {
return c.viewer.close()
}
// remoteView provides an interface for fetching a summary.json and copying
// files from a remote host after a runtests invocation.
type remoteViewer interface {
summary(string) (*TestSummary, error)
copyFile(string, string) error
close() error
}
type sftpViewer struct {
client *sftp.Client
}
func (v sftpViewer) summary(summaryPath string) (*TestSummary, error) {
f, err := v.client.Open(summaryPath)
if err != nil {
return nil, err
}
defer f.Close()
var summary TestSummary
if err = json.NewDecoder(f).Decode(&summary); err != nil {
return nil, err
}
return &summary, nil
}
func (v sftpViewer) copyFile(remote, local string) error {
remoteFile, err := v.client.Open(remote)
if err != nil {
return err
}
defer remoteFile.Close()
if err = os.MkdirAll(filepath.Dir(local), 0777); err != nil {
return err
}
localFile, err := os.Create(local)
if err != nil {
return err
}
defer localFile.Close()
_, err = io.Copy(localFile, remoteFile)
return err
}
func (v sftpViewer) close() error {
return v.client.Close()
}
// CopyDataSinks retrieves the summary.json produced by runtests (assuming only
// a single test was run) and copies the data sinks specified in the summary
// from the remoteOutputDir on the target to the localOutputDir on the host. It
// modifies the output data sinks in-place, referencing the local paths of the
// copied files.
func copyDataSinks(viewer remoteViewer, remoteOutputDir, localOutputDir string) (DataSinkMap, error) {
summaryPath := path.Join(remoteOutputDir, TestSummaryFilename)
summary, err := viewer.summary(summaryPath)
if err != nil {
return nil, fmt.Errorf("failed to read test summary from %q: %w", summaryPath, err)
}
sinks := DataSinkMap{}
for _, details := range summary.Tests {
for name, files := range details.DataSinks {
var outputFiles []DataSink
for _, file := range files {
src := path.Join(remoteOutputDir, file.File)
dest := filepath.Join(localOutputDir, file.File)
if err = viewer.copyFile(src, dest); err != nil {
return nil, fmt.Errorf("failed to copy data sink %q: %w", file.File, err)
}
outputFiles = append(outputFiles, DataSink{
Name: file.Name,
File: file.File,
})
}
sinks[name] = outputFiles
}
}
return sinks, nil
}