Commit 5ef4fe1c authored by Erik Jensen's avatar Erik Jensen Committed by Commit Bot

Create new FileOperations interface

This creates a basic FileOperations interface for use with file
transfers that should be relatively straightforward to proxy over IPC.

It also creates a ThreadedFileOperations implementation of the
FileOperations interface using FileProxy, which will eventually replace
FileProxyWrapper. This implementation should be platform agnostic. A
future CL will add an IpcFileOperations implementation for Windows, which
will proxy operations from the network process to a ThreadedFileOperation
instance in the desktop process.

Bug: 679313
Change-Id: Ic7d6ce35f498e469f86f9ed7a8fbcc8bd0cafda9
Reviewed-on: https://chromium-review.googlesource.com/c/1325213Reviewed-by: default avatarJoe Downing <joedow@chromium.org>
Commit-Queue: Erik Jensen <rkjnsn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#612377}
parent 7edce81a
...@@ -508,6 +508,7 @@ source_set("unit_tests") { ...@@ -508,6 +508,7 @@ source_set("unit_tests") {
deps = [ deps = [
":host", ":host",
":test_support", ":test_support",
"//remoting/host/file_transfer:unit_tests",
"//remoting/host/it2me:common", "//remoting/host/it2me:common",
"//remoting/host/native_messaging", "//remoting/host/native_messaging",
"//remoting/host/security_key:unit_tests", "//remoting/host/security_key:unit_tests",
......
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//remoting/build/config/remoting_build.gni")
source_set("file_transfer") {
public = [
"file_operations.h",
"local_file_operations.h",
]
sources = [
"local_file_operations.cc",
]
deps = [
"//base",
"//remoting/protocol",
]
}
source_set("unit_tests") {
testonly = true
sources = [
"local_file_operations_unittest.cc",
]
deps = [
":file_transfer",
"//remoting/protocol",
"//testing/gtest",
]
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef REMOTING_HOST_FILE_TRANSFER_FILE_OPERATIONS_H_
#define REMOTING_HOST_FILE_TRANSFER_FILE_OPERATIONS_H_
#include <cstddef>
#include <memory>
#include <string>
#include "base/callback.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/optional.h"
#include "remoting/proto/file_transfer.pb.h"
namespace remoting {
// Interface for reading and writing file transfers.
class FileOperations {
public:
enum State {
// The file has been opened. WriteChunk(), ReadChunk(), and Close() can be
// called.
kReady = 0,
// A file operation is currently being processed. WriteChunk(), ReadChunk(),
// and Close() cannot be called until the state changes back to kReady.
kBusy = 1,
// Close() has been called and succeeded.
kClosed = 2,
// Cancel() has been called or an error occured.
kFailed = 3,
};
class Writer {
public:
// |error| will be nullopt on success or contain error details on failure.
using Callback = base::OnceCallback<void(
base::Optional<protocol::FileTransfer_Error> error)>;
// Destructing FileWriter before calling Close will implicitly call Cancel.
virtual ~Writer() {}
// Writes a chuck to the file. Chunks cannot be queued; the caller must
// wait until callback is called before calling WriteChunk again or calling
// Close.
virtual void WriteChunk(std::string data, Callback callback) = 0;
// Closes the file, flushing any data still in the OS buffer and moving the
// the file to its final location.
virtual void Close(Callback callback) = 0;
// Cancels writing the file. The partially written file will be deleted. May
// be called at any time (including when an operation is pending).
virtual void Cancel() = 0;
virtual State state() = 0;
};
class Reader {
public:
// |error| will be nullopt on success or contain error details on failure.
// In the event of an error, |data| will contain the data successfully read
// before the error, if any.
using Callback = base::OnceCallback<void(
base::Optional<protocol::FileTransfer_Error> error,
std::string data)>;
virtual ~Reader() {}
// Reads a chunk of the given size from the file.
virtual void ReadChunk(std::size_t size, Callback callback) = 0;
virtual void Close() = 0;
virtual State state() = 0;
};
// On success, |error| will be nullopt and |writer| can be used to write data
// to the file. On failure, |error| will contain the error details and
// |writer| will be null.
using WriteFileCallback = base::OnceCallback<void(
base::Optional<protocol::FileTransfer_Error> error,
std::unique_ptr<Writer> writer)>;
// On success, |error| will be nullopt and |reader| can be used to read data
// from the file. On failure, |error| will contain the error details and
// |reader| will be null.
using ReadFileCallback = base::OnceCallback<void(
base::Optional<protocol::FileTransfer_Error> error,
std::unique_ptr<Reader> reader)>;
virtual ~FileOperations() {}
// Starts writing a new file to the default location. This will create a temp
// file at the location, which will be renamed when writing is complete.
virtual void WriteFile(const base::FilePath& filename,
WriteFileCallback callback) = 0;
// Prompt the user to select a file and start reading it.
virtual void ReadFile(ReadFileCallback) = 0;
};
} // namespace remoting
#endif // REMOTING_HOST_FILE_TRANSFER_FILE_OPERATIONS_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/file_transfer/local_file_operations.h"
#include <cstdint>
#include "base/bind.h"
#include "base/files/file_proxy.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/path_service.h"
#include "base/sequence_checker.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/stringprintf.h"
#include "base/task/post_task.h"
#include "base/task_runner_util.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "remoting/protocol/file_transfer_helpers.h"
namespace remoting {
namespace {
constexpr char kTempFileExtension[] = ".crdownload";
remoting::protocol::FileTransfer_Error_Type FileErrorToResponseErrorType(
base::File::Error file_error) {
switch (file_error) {
case base::File::FILE_ERROR_ACCESS_DENIED:
return remoting::protocol::FileTransfer_Error_Type_PERMISSION_DENIED;
case base::File::FILE_ERROR_NO_SPACE:
return remoting::protocol::FileTransfer_Error_Type_OUT_OF_DISK_SPACE;
default:
return remoting::protocol::FileTransfer_Error_Type_IO_ERROR;
}
}
class LocalFileWriter : public FileOperations::Writer {
public:
~LocalFileWriter() override;
// FileOperations::FileWriter implementation
void WriteChunk(std::string data, Callback callback) override;
void Close(Callback callback) override;
void Cancel() override;
FileOperations::State state() override;
static void WriteFile(const base::FilePath& filename,
FileOperations::WriteFileCallback callback);
private:
LocalFileWriter(scoped_refptr<base::SequencedTaskRunner> file_task_runner,
std::unique_ptr<base::FileProxy> file_proxy,
const base::FilePath& destination_filepath);
// Callbacks for CreateFile(). These are static because they're used in the
// construction of TheadedFileWriter.
static void CreateTempFile(std::unique_ptr<LocalFileWriter> writer,
FileOperations::WriteFileCallback callback,
int unique_path_number);
static void OnCreateResult(std::unique_ptr<LocalFileWriter> writer,
FileOperations::WriteFileCallback callback,
base::File::Error error);
void OnWriteResult(std::string data,
Callback callback,
base::File::Error error,
int bytes_written);
// Callbacks for Close().
void OnCloseResult(Callback callback, base::File::Error error);
void MoveToDestination(Callback callback, int unique_path_number);
void OnMoveResult(Callback callback, bool success);
void SetState(FileOperations::State state);
FileOperations::State state_ = FileOperations::kReady;
base::FilePath destination_filepath_;
base::FilePath temp_filepath_;
std::uint64_t bytes_written_ = 0;
scoped_refptr<base::SequencedTaskRunner> file_task_runner_;
std::unique_ptr<base::FileProxy> file_proxy_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<LocalFileWriter> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(LocalFileWriter);
};
LocalFileWriter::LocalFileWriter(
scoped_refptr<base::SequencedTaskRunner> file_task_runner,
std::unique_ptr<base::FileProxy> file_proxy,
const base::FilePath& destination_filepath)
: destination_filepath_(std::move(destination_filepath)),
temp_filepath_(
destination_filepath_.AddExtensionASCII(kTempFileExtension)),
file_task_runner_(std::move(file_task_runner)),
file_proxy_(std::move(file_proxy)),
weak_factory_(this) {}
LocalFileWriter::~LocalFileWriter() {
Cancel();
}
void LocalFileWriter::WriteChunk(std::string data, Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(FileOperations::kReady, state_);
SetState(FileOperations::kBusy);
// TODO(rkjnsn): Under what circumstances can posting the task fail? Is it
// worth checking for? If so, what should we do in that case,
// given that callback is moved into the task and not returned
// on error?
// Ensure buffer pointer is obtained before data is moved.
const char* buffer = data.data();
const std::size_t size = data.size();
file_proxy_->Write(bytes_written_, buffer, size,
base::BindOnce(&LocalFileWriter::OnWriteResult,
weak_factory_.GetWeakPtr(), std::move(data),
std::move(callback)));
}
void LocalFileWriter::Close(Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(FileOperations::kReady, state_);
SetState(FileOperations::kBusy);
file_proxy_->Close(base::BindOnce(&LocalFileWriter::OnCloseResult,
weak_factory_.GetWeakPtr(),
std::move(callback)));
}
void LocalFileWriter::Cancel() {
if (state_ == FileOperations::kClosed || state_ == FileOperations::kFailed) {
return;
}
// Ensure we don't receive further callbacks.
weak_factory_.InvalidateWeakPtrs();
// Drop FileProxy, which will close the underlying file on the file sequence
// after any possible pending operation is complete.
file_proxy_.reset();
// And finally delete the temp file.
file_task_runner_->PostTask(
FROM_HERE, base::BindOnce(base::IgnoreResult(&base::DeleteFile),
temp_filepath_, false /* recursive */));
SetState(FileOperations::kFailed);
}
FileOperations::State LocalFileWriter::state() {
return state_;
}
void LocalFileWriter::WriteFile(const base::FilePath& filename,
FileOperations::WriteFileCallback callback) {
base::FilePath target_directory;
if (!base::PathService::Get(base::DIR_USER_DESKTOP, &target_directory)) {
LOG(ERROR) << "Failed to get DIR_USER_DESKTOP from base::PathService::Get";
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
protocol::MakeFileTransferError(
FROM_HERE, protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR),
nullptr));
return;
}
scoped_refptr<base::SequencedTaskRunner> file_task_runner =
base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
DCHECK(file_task_runner);
base::SequencedTaskRunner* file_task_runner_ptr = file_task_runner.get();
auto file_proxy = std::make_unique<base::FileProxy>(file_task_runner.get());
base::FilePath destination_filepath =
target_directory.Append(filename.BaseName());
auto writer = base::WrapUnique(
new LocalFileWriter(std::move(file_task_runner), std::move(file_proxy),
destination_filepath));
// Ensure path reference is obtained before writer is moved.
const base::FilePath& temp_filepath = writer->temp_filepath_;
// Passing file_task_runner_ptr and temp_filepath is safe because they are
// kept alive by writer, which is bound to the callback.
PostTaskAndReplyWithResult(
file_task_runner_ptr, FROM_HERE,
base::BindOnce(&base::GetUniquePathNumber, temp_filepath,
base::FilePath::StringType()),
base::BindOnce(&CreateTempFile, std::move(writer), std::move(callback)));
}
void LocalFileWriter::CreateTempFile(std::unique_ptr<LocalFileWriter> writer,
FileOperations::WriteFileCallback callback,
int unique_path_number) {
if (unique_path_number > 0) {
writer->temp_filepath_ = writer->temp_filepath_.InsertBeforeExtensionASCII(
base::StringPrintf(" (%d)", unique_path_number));
}
// Ensure needed pointers/references are obtained before writer is moved.
base::FileProxy* file_proxy_ptr = writer->file_proxy_.get();
const base::FilePath& temp_filepath = writer->temp_filepath_;
// FLAG_SHARE_DELETE allows the file to be marked as deleted on Windows while
// the handle is still open. (Other OS's allow this by default.) This allows
// Cancel to clean up the temporary file even if there are writes pending.
file_proxy_ptr->CreateOrOpen(
temp_filepath,
base::File::FLAG_CREATE | base::File::FLAG_WRITE |
base::File::FLAG_SHARE_DELETE,
base::BindOnce(&OnCreateResult, std::move(writer), std::move(callback)));
}
void LocalFileWriter::OnCreateResult(std::unique_ptr<LocalFileWriter> writer,
FileOperations::WriteFileCallback callback,
base::File::Error error) {
if (error != base::File::FILE_OK) {
LOG(ERROR) << "Creating temp file failed with error: " << error;
std::move(callback).Run(
protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error),
nullptr);
} else {
// Now that the temp file has been created successfully, we could lock it
// using base::File::Lock(), but this would not prevent the file from being
// deleted. When the file is deleted, WriteChunk() will continue to write to
// the file as if the file was still there, and an error will occur when
// calling base::Move() to move the temp file. Chrome exhibits the same
// behavior with its downloads.
std::move(callback).Run(base::nullopt, std::move(writer));
}
}
void LocalFileWriter::OnWriteResult(std::string data,
Callback callback,
base::File::Error error,
int bytes_written) {
if (error != base::File::FILE_OK) {
LOG(ERROR) << "Write failed with error: " << error;
Cancel();
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
SetState(FileOperations::kReady);
bytes_written_ += bytes_written;
// bytes_written should never be negative if error is FILE_OK.
if (static_cast<std::size_t>(bytes_written) != data.size()) {
// Write already makes a "best effort" to write all of the data, so this
// probably means that an error occurred. Unfortunately, the only way to
// find out what went wrong is to try again.
// TODO(rkjnsn): Would it be better just to return a generic error, here?
WriteChunk(data.substr(bytes_written), std::move(callback));
return;
}
std::move(callback).Run(base::nullopt);
}
void LocalFileWriter::OnCloseResult(Callback callback,
base::File::Error error) {
if (error != base::File::FILE_OK) {
LOG(ERROR) << "Close failed with error: " << error;
Cancel();
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
base::PostTaskAndReplyWithResult(
file_task_runner_.get(), FROM_HERE,
base::BindOnce(&base::GetUniquePathNumber, destination_filepath_,
base::FilePath::StringType()),
base::BindOnce(&LocalFileWriter::MoveToDestination,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void LocalFileWriter::MoveToDestination(Callback callback,
int unique_path_number) {
if (unique_path_number > 0) {
destination_filepath_ = destination_filepath_.InsertBeforeExtensionASCII(
base::StringPrintf(" (%d)", unique_path_number));
}
PostTaskAndReplyWithResult(
file_task_runner_.get(), FROM_HERE,
base::BindOnce(&base::Move, temp_filepath_, destination_filepath_),
base::BindOnce(&LocalFileWriter::OnMoveResult, weak_factory_.GetWeakPtr(),
std::move(callback)));
}
void LocalFileWriter::OnMoveResult(Callback callback, bool success) {
if (success) {
SetState(FileOperations::kClosed);
std::move(callback).Run(base::nullopt);
} else {
LOG(ERROR) << "Failed to move file to final destination.";
Cancel();
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, protocol::FileTransfer_Error_Type_IO_ERROR));
}
}
void LocalFileWriter::SetState(FileOperations::State state) {
switch (state) {
case FileOperations::kReady:
DCHECK(state_ == FileOperations::kBusy);
break;
case FileOperations::kBusy:
DCHECK_EQ(state_, FileOperations::kReady);
break;
case FileOperations::kClosed:
DCHECK(state_ == FileOperations::kBusy);
break;
case FileOperations::kFailed:
// Any state can change to kFailed.
break;
}
state_ = state;
}
} // namespace
void LocalFileOperations::WriteFile(
const base::FilePath& filename,
FileOperations::WriteFileCallback callback) {
LocalFileWriter::WriteFile(filename, std::move(callback));
}
void LocalFileOperations::ReadFile(FileOperations::ReadFileCallback) {
NOTIMPLEMENTED();
}
} // namespace remoting
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef REMOTING_HOST_FILE_TRANSFER_LOCAL_FILE_OPERATIONS_H_
#define REMOTING_HOST_FILE_TRANSFER_LOCAL_FILE_OPERATIONS_H_
#include "remoting/host/file_transfer/file_operations.h"
namespace remoting {
// Implementation of FileOperations that uses base::FileProxy to perform file
// operations on a dedicated thread.
class LocalFileOperations : public FileOperations {
public:
void WriteFile(const base::FilePath& filename,
WriteFileCallback callback) override;
void ReadFile(ReadFileCallback) override;
};
} // namespace remoting
#endif // REMOTING_HOST_FILE_TRANSFER_LOCAL_FILE_OPERATIONS_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/file_transfer/local_file_operations.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/containers/queue.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/test/scoped_path_override.h"
#include "base/test/scoped_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace remoting {
class LocalFileOperationsTest : public testing::Test {
public:
LocalFileOperationsTest();
// testing::Test implementation.
void SetUp() override;
void TearDown() override;
protected:
const base::FilePath kTestFilename =
base::FilePath::FromUTF8Unsafe("test-file.txt");
const base::FilePath kTestFilenameSecondary =
base::FilePath::FromUTF8Unsafe("test-file (1).txt");
const std::string kTestDataOne = "this is the first test string";
const std::string kTestDataTwo = "this is the second test string";
const std::string kTestDataThree = "this is the third test string";
base::FilePath TestDir();
void WriteFile(const base::FilePath& filename,
base::queue<std::string> chunks,
bool close);
void OnFileCreated(base::queue<std::string> chunks,
bool close,
base::Optional<protocol::FileTransfer_Error> error,
std::unique_ptr<FileOperations::Writer> writer);
void OnWriteComplete(base::queue<std::string> remaining_chunks,
bool close,
base::Optional<protocol::FileTransfer_Error> error);
void OnCloseComplete(base::Optional<protocol::FileTransfer_Error> error);
base::test::ScopedTaskEnvironment scoped_task_environment_;
base::ScopedPathOverride scoped_path_override_;
std::unique_ptr<FileOperations> file_operations_;
std::unique_ptr<FileOperations::Writer> file_writer_;
bool operation_completed_ = false;
};
LocalFileOperationsTest::LocalFileOperationsTest()
: scoped_task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::DEFAULT,
base::test::ScopedTaskEnvironment::ExecutionMode::QUEUED),
// Points DIR_USER_DESKTOP at a scoped temporary directory.
scoped_path_override_(base::DIR_USER_DESKTOP),
file_operations_(std::make_unique<LocalFileOperations>()) {}
void LocalFileOperationsTest::SetUp() {}
void LocalFileOperationsTest::TearDown() {}
base::FilePath LocalFileOperationsTest::TestDir() {
base::FilePath result;
EXPECT_TRUE(base::PathService::Get(base::DIR_USER_DESKTOP, &result));
return result;
}
void LocalFileOperationsTest::WriteFile(const base::FilePath& filename,
base::queue<std::string> chunks,
bool close) {
operation_completed_ = false;
file_operations_->WriteFile(
filename,
base::BindOnce(&LocalFileOperationsTest::OnFileCreated,
base::Unretained(this), std::move(chunks), close));
}
void LocalFileOperationsTest::OnFileCreated(
base::queue<std::string> chunks,
bool close,
base::Optional<protocol::FileTransfer_Error> error,
std::unique_ptr<FileOperations::Writer> writer) {
file_writer_ = std::move(writer);
OnWriteComplete(std::move(chunks), close, error);
}
void LocalFileOperationsTest::OnWriteComplete(
base::queue<std::string> remaining_chunks,
bool close,
base::Optional<protocol::FileTransfer_Error> error) {
ASSERT_FALSE(error);
if (!remaining_chunks.empty()) {
std::string next_chunk = std::move(remaining_chunks.front());
remaining_chunks.pop();
file_writer_->WriteChunk(
std::move(next_chunk),
base::BindOnce(&LocalFileOperationsTest::OnWriteComplete,
base::Unretained(this), std::move(remaining_chunks),
close));
} else if (close) {
file_writer_->Close(base::BindOnce(
&LocalFileOperationsTest::OnCloseComplete, base::Unretained(this)));
} else {
operation_completed_ = true;
}
}
void LocalFileOperationsTest::OnCloseComplete(
base::Optional<protocol::FileTransfer_Error> error) {
ASSERT_FALSE(error);
operation_completed_ = true;
}
// Verifies that a file consisting of three chunks can be written successfully.
TEST_F(LocalFileOperationsTest, WritesThreeChunks) {
WriteFile(
kTestFilename,
base::queue<std::string>({kTestDataOne, kTestDataTwo, kTestDataThree}),
true /* close */);
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(operation_completed_);
std::string actual_file_data;
ASSERT_TRUE(base::ReadFileToString(TestDir().Append(kTestFilename),
&actual_file_data));
ASSERT_EQ(kTestDataOne + kTestDataTwo + kTestDataThree, actual_file_data);
}
// Verifies that LocalFileOperations will write to a file named
// "file (1).txt" if "file.txt" already exists.
TEST_F(LocalFileOperationsTest, RenamesFileIfExists) {
WriteFile(kTestFilename, base::queue<std::string>({kTestDataOne}),
true /* close */);
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(operation_completed_);
WriteFile(kTestFilename, base::queue<std::string>({kTestDataTwo}),
true /* close */);
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(operation_completed_);
std::string actual_file_data_one;
ASSERT_TRUE(base::ReadFileToString(TestDir().Append(kTestFilename),
&actual_file_data_one));
ASSERT_EQ(kTestDataOne, actual_file_data_one);
std::string actual_file_data_two;
ASSERT_TRUE(base::ReadFileToString(TestDir().Append(kTestFilenameSecondary),
&actual_file_data_two));
ASSERT_EQ(kTestDataTwo, actual_file_data_two);
}
// Verifies that calling Cancel() deletes the temporary file.
TEST_F(LocalFileOperationsTest, CancelDeletesTemp) {
WriteFile(
kTestFilename,
base::queue<std::string>({kTestDataOne, kTestDataTwo, kTestDataThree}),
false /* close */);
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(operation_completed_);
file_writer_->Cancel();
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(base::IsDirectoryEmpty(TestDir()));
}
// Verifies that Cancel() can be called while an operation is pending.
TEST_F(LocalFileOperationsTest, CancelsWhileOperationPending) {
WriteFile(kTestFilename, base::queue<std::string>({kTestDataOne}),
false /* close */);
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(operation_completed_);
file_writer_->WriteChunk(kTestDataTwo, base::DoNothing());
file_writer_->Cancel();
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(base::IsDirectoryEmpty(TestDir()));
}
// Verifies that dropping an unclosed FileWriter is the same as canceling it.
TEST_F(LocalFileOperationsTest, CancelsWhenDestructed) {
WriteFile(
kTestFilename,
base::queue<std::string>({kTestDataOne, kTestDataTwo, kTestDataThree}),
false /* close */);
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(operation_completed_);
file_writer_->WriteChunk(kTestDataTwo, base::DoNothing());
file_writer_.reset();
scoped_task_environment_.RunUntilIdle();
ASSERT_TRUE(base::IsDirectoryEmpty(TestDir()));
}
} // namespace remoting
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment