blob: bcda5da411f359bffcff6ced87361210d441330f [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"
"go.fuchsia.dev/fuchsia/tools/net/sshutil"
)
// DataSinkCopier copies data sinks from a remote host after a runtests invocation.
type DataSinkCopier struct {
viewer remoteViewer
sshClient *sshutil.Client
}
// NewDataSinkCopier constructs a copier using the specified ssh client.
func NewDataSinkCopier(client *sshutil.Client) (*DataSinkCopier, error) {
sftpClient, err := client.NewSFTPClient()
if err != nil {
return nil, err
}
viewer := &sftpViewer{sftpClient}
copier := &DataSinkCopier{
viewer: viewer,
sshClient: client,
}
return copier, nil
}
// Copy copies data sinks using the copier's remote viewer.
func (c DataSinkCopier) Copy(references []DataSinkReference, localDir string) (DataSinkMap, error) {
return copyDataSinks(c.viewer, references, localDir)
}
// GetReferences returns a map of test name to a reference to the remote data sinks.
func (c DataSinkCopier) GetReferences(remoteDir string) (map[string]DataSinkReference, error) {
return getDataSinkReferences(c.viewer, remoteDir)
}
// Reconnect should be called after the sshClient has been disconnected and
// reconnected. It closes the old viewer and creates a new viewer using the
// refreshed sshClient.
func (c *DataSinkCopier) Reconnect() error {
// This may fail because the underlying ssh session has already been
// closed, which is fine and expected, so no need to check the
// returned error.
c.viewer.close()
sftpClient, err := c.sshClient.NewSFTPClient()
if err != nil {
return fmt.Errorf("failed to create new SFTP client: %w", err)
}
c.viewer = &sftpViewer{sftpClient}
return nil
}
func (c DataSinkCopier) Close() error {
return c.viewer.close()
}
// DataSinkReference holds information about data sinks on the target.
type DataSinkReference struct {
Sinks DataSinkMap
RemoteDir string
}
// Size returns the number of sinks held by the reference.
func (d DataSinkReference) Size() int {
numSinks := 0
for _, files := range d.Sinks {
numSinks += len(files)
}
return numSinks
}
// 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), 0o777); 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()
}
// GetDataSinkReferences retrieves the summary.json written to the
// `remoteOutputDir` and gets the data sinks specified in the summary.
func getDataSinkReferences(viewer remoteViewer, remoteOutputDir string) (map[string]DataSinkReference, error) {
sinksPerTest := make(map[string]DataSinkReference)
summaryPath := path.Join(remoteOutputDir, TestSummaryFilename)
summary, err := viewer.summary(summaryPath)
if err != nil {
return sinksPerTest, fmt.Errorf("failed to read test summary from %q: %w", summaryPath, err)
}
for _, details := range summary.Tests {
sinksPerTest[details.Name] = DataSinkReference{details.DataSinks, remoteOutputDir}
}
return sinksPerTest, nil
}
// CopyDataSinks copies the data sinks specified in references from the
// remoteOutputDir on the target to the localOutputDir on the host.
// It returns a DataSinkMap of the copied files, removing duplicates across
// the references.
func copyDataSinks(viewer remoteViewer, references []DataSinkReference, localOutputDir string) (DataSinkMap, error) {
sinks := DataSinkMap{}
copied := make(map[string]struct{})
for _, ref := range references {
for name, files := range ref.Sinks {
if _, ok := sinks[name]; !ok {
sinks[name] = []DataSink{}
}
for _, file := range files {
if _, ok := copied[file.File]; ok {
continue
}
src := path.Join(ref.RemoteDir, 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 from %s: %w", file.File, ref.RemoteDir, err)
}
copied[file.File] = struct{}{}
sinks[name] = append(sinks[name], file)
}
}
}
return sinks, nil
}