1 /*
2 * Copyright 2017 Google LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "fcp/testing/testing.h"
18
19 #include <stdio.h>
20 #include <stdlib.h>
21
22 #include <filesystem>
23 #include <string>
24
25 #include "gtest/gtest.h"
26 #include "absl/flags/flag.h"
27 #include "absl/status/status.h"
28 #include "absl/strings/str_cat.h"
29 #include "absl/strings/str_replace.h"
30 #include "absl/strings/string_view.h"
31 #include "fcp/base/base_name.h"
32 #include "fcp/base/monitoring.h"
33 #include "fcp/base/platform.h"
34 #include "fcp/testing/tracing_schema.h"
35 #include "fcp/tracing/tracing_span.h"
36
37 namespace fcp {
38
TestName()39 std::string TestName() {
40 auto test_info = testing::UnitTest::GetInstance()->current_test_info();
41 return absl::StrReplaceAll(test_info->name(), {{"/", "_"}});
42 }
43
TestCaseName()44 std::string TestCaseName() {
45 auto test_info = testing::UnitTest::GetInstance()->current_test_info();
46 return absl::StrReplaceAll(test_info->test_case_name(), {{"/", "_"}});
47 }
48
GetTestDataPath(absl::string_view relative_path)49 std::string GetTestDataPath(absl::string_view relative_path) {
50 auto env = getenv("TEST_SRCDIR");
51 std::string test_srcdir = env ? env : "";
52 return ConcatPath(test_srcdir, ConcatPath("com_google_fcp", relative_path));
53 }
54
TemporaryTestFile(absl::string_view suffix)55 std::string TemporaryTestFile(absl::string_view suffix) {
56 return ConcatPath(StripTrailingPathSeparator(testing::TempDir()),
57 absl::StrCat(TestName(), suffix));
58 }
59
60 namespace {
61
EnsureDirExists(absl::string_view path)62 absl::Status EnsureDirExists(absl::string_view path) {
63 if (FileExists(path)) {
64 return absl::OkStatus();
65 }
66 auto path_str = std::string(path);
67 int error;
68 #ifndef _WIN32
69 error = mkdir(path_str.c_str(), 0733);
70 #else
71 error = _mkdir(path_str.c_str());
72 #endif
73 if (error) {
74 return absl::InternalError(absl::StrCat(
75 "cannot create directory ", path_str, "(error code ", error, ")"));
76 }
77 return absl::OkStatus();
78 }
79
80 } // namespace
81
ShouldUpdateBaseline()82 bool ShouldUpdateBaseline() {
83 return getenv("FCP_UPDATE_BASELINE");
84 }
85
86 namespace {
87
MakeTempFileName()88 std::string MakeTempFileName() {
89 #ifdef __APPLE__
90 // Apple has marked tmpnam as deprecated. As we are compiling with -Werror,
91 // turning this off for this case. Apple recommends to use mkstemp instead,
92 // but because this opens a file, it's not exactly what we want, and it's not
93 // portable. std::filesystem in C++17 should fix this issue.
94 #pragma clang diagnostic push
95 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
96 #endif
97 return tmpnam(nullptr);
98 #ifdef __APPLE__
99 #pragma clang diagnostic pop
100 #endif
101 }
102
ShellCommand(absl::string_view command,std::string * stdout_result,std::string * stderr_result)103 absl::Status ShellCommand(absl::string_view command, std::string* stdout_result,
104 std::string* stderr_result) {
105 #ifdef _WIN32
106 return absl::UnimplementedError("ShellCommand not implemented for Windows");
107 #else
108 // Prepare command for output redirection.
109 std::string command_str = std::string(command);
110 std::string stdout_file;
111 if (stdout_result != nullptr) {
112 stdout_file = MakeTempFileName();
113 absl::StrAppend(&command_str, " 1>", stdout_file);
114 }
115 std::string stderr_file;
116 if (stderr_result != nullptr) {
117 stderr_file = MakeTempFileName();
118 absl::StrAppend(&command_str, " 2>", stderr_file);
119 }
120
121 // Call the command.
122 int result = std::system(command_str.c_str());
123
124 // Read and remove redirected output.
125 if (stdout_result != nullptr) {
126 auto status_or_result = ReadFileToString(stdout_file);
127 if (status_or_result.ok()) {
128 *stdout_result = status_or_result.value();
129 std::remove(stdout_file.c_str());
130 } else {
131 *stdout_result = "";
132 }
133 }
134 if (stderr_result != nullptr) {
135 auto status_or_result = ReadFileToString(stderr_file);
136 if (status_or_result.ok()) {
137 *stderr_result = status_or_result.value();
138 std::remove(stderr_file.c_str());
139 } else {
140 *stderr_result = "";
141 }
142 }
143
144 // Construct result.
145 if (result != 0) {
146 return absl::InternalError(absl::StrCat(
147 "command execution failed: ", command_str, " returns ", result));
148 } else {
149 return absl::OkStatus();
150 }
151 #endif
152 }
153
154 } // namespace
155
ComputeDiff(absl::string_view baseline_file,absl::string_view content)156 absl::StatusOr<std::string> ComputeDiff(absl::string_view baseline_file,
157 absl::string_view content) {
158 std::string diff_result;
159 std::string baseline_file_str = GetTestDataPath(baseline_file);
160 if (!FileExists(baseline_file_str)) {
161 diff_result = absl::StrCat("no recorded baseline file ", baseline_file_str);
162 } else {
163 #ifndef _WIN32
164 // Expect Unix diff command to be available.
165 auto provided_file = TemporaryTestFile(".provided");
166 auto status = WriteStringToFile(provided_file, content);
167 if (!status.ok()) {
168 return status;
169 }
170 std::string std_out, std_err;
171 status = ShellCommand(
172 absl::StrCat("diff -u ", baseline_file_str, " ", provided_file),
173 &std_out, &std_err);
174 std::remove(provided_file.c_str());
175 if (status.code() != OK) {
176 if (!std_err.empty()) {
177 // Indicates a failure in diff execution itself.
178 return absl::InternalError(absl::StrCat("command failed: ", std_err));
179 }
180 diff_result = std_out;
181 }
182 #else // _WIN32
183 // For now we do a simple string compare on Windows.
184 auto status_or_string = ReadFileToString(baseline_file_str);
185 if (!status_or_string.ok()) {
186 return status_or_string.status();
187 }
188 if (status_or_string.value() != content) {
189 diff_result = "baseline and actual differ (see respective files)";
190 }
191 #endif
192 }
193 return diff_result;
194 }
195
VerifyAgainstBaseline(absl::string_view baseline_file,absl::string_view content)196 StatusOr<std::string> VerifyAgainstBaseline(absl::string_view baseline_file,
197 absl::string_view content) {
198 auto status_or_diff_result = ComputeDiff(baseline_file, content);
199 if (!status_or_diff_result.ok()) {
200 return status_or_diff_result;
201 }
202 auto& diff_result = status_or_diff_result.value();
203 if (diff_result.empty()) {
204 // success
205 return status_or_diff_result;
206 }
207
208 // Determine the location where to store the new baseline.
209 std::string new_baseline_file;
210 bool auto_update = false;
211
212 if (new_baseline_file.empty() && ShouldUpdateBaseline()) {
213 new_baseline_file = GetTestDataPath(baseline_file);
214 diff_result =
215 absl::StrCat("\nAutomatically updated baseline file: ", baseline_file);
216 auto_update = true;
217 }
218
219 if (new_baseline_file.empty()) {
220 // Store new baseline file in a TMP location.
221 #ifndef _WIN32
222 const char* temp_dir = "/tmp";
223 #else
224 const char* temp_dir = getenv("TEMP");
225 #endif
226 auto temp_output_dir =
227 ConcatPath(temp_dir, absl::StrCat("fcp_", TestCaseName()));
228 FCP_CHECK_STATUS(EnsureDirExists(temp_output_dir));
229 new_baseline_file = ConcatPath(temp_output_dir, BaseName(baseline_file));
230 absl::StrAppend(&diff_result, "\nNew baseline file: ", new_baseline_file);
231 absl::StrAppend(&diff_result, "\nTo update, use:");
232 absl::StrAppend(&diff_result, "\n\n cp ", new_baseline_file, " ",
233 baseline_file, "\n");
234 }
235
236 if (!auto_update) {
237 absl::StrAppend(&diff_result,
238 "\nTo automatically update baseline files, use");
239 absl::StrAppend(&diff_result,
240 "\nenvironment variable FCP_UPDATE_BASELINE.");
241 }
242
243 // Write the new baseline.
244 auto status = WriteStringToFile(new_baseline_file, content);
245 if (!status.ok()) {
246 return status;
247 }
248
249 // Deliver result.
250 if (auto_update) {
251 FCP_LOG(INFO) << diff_result;
252 diff_result = ""; // make test pass
253 }
254 return diff_result;
255 }
256
IsCode(StatusCode code)257 StatusMatcher IsCode(StatusCode code) { return StatusMatcher(code); }
IsOk()258 StatusMatcher IsOk() { return IsCode(OK); }
259
TraceTestError(SourceLocation loc)260 Error TraceTestError(SourceLocation loc) {
261 return TraceError<TestError>(loc.file_name(), loc.line());
262 }
263
264 } // namespace fcp
265