xref: /aosp_15_r20/external/pigweed/pw_stream/std_file_stream_test.cc (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1 // Copyright 2022 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14 
15 #include "pw_stream/std_file_stream.h"
16 
17 #include <algorithm>
18 #include <array>
19 #include <cinttypes>
20 #include <cstdio>
21 #include <filesystem>
22 #include <random>
23 #include <string>
24 #include <string_view>
25 
26 #include "pw_assert/assert.h"
27 #include "pw_bytes/span.h"
28 #include "pw_containers/algorithm.h"
29 #include "pw_random/xor_shift.h"
30 #include "pw_span/span.h"
31 #include "pw_status/status.h"
32 #include "pw_status/status_with_size.h"
33 #include "pw_string/string_builder.h"
34 #include "pw_unit_test/framework.h"
35 
36 namespace pw::stream {
37 namespace {
38 
39 constexpr std::string_view kSmallTestData(
40     "This is a test string used to verify correctness!");
41 
42 // Creates a directory with a specified prefix followed by a random 32-bit hex
43 // number. Random temporary file handle names can then be requested. When the
44 // TempDir is destroyed, the entire directory is deleted.
45 //
46 // Example created temporary files:
47 //     /tmp/StdFileStreamTest32B37409/997BDDA2
48 //     /tmp/StdFileStreamTest32B37409/C181909B
49 //
50 // WARNING: This class should ONLY be used for these tests!
51 //
52 // These tests need to open and close files by file name, which is incompatible
53 // with std::tmpfile() (which deletes files on close). Even though std::tmpnam()
54 // looks like the right tool to use, it's not thread safe and doesn't provide
55 // any guarantees that the provided file name is not in use. std::tmpnam() is
56 // also marked with a deprecation warning on some systems, warning against using
57 // it at all.
58 //
59 // While on some systems this approach may provide significantly better
60 // uniqueness since std::random_device may be backed with thread-safe random
61 // sources, the STL does not explicitly require std::random_device to produce
62 // non-deterministic random data (instead only recommending it). If
63 // std::random_device is pseudo-random, this temporary directory will always
64 // end up with the same naming pattern.
65 //
66 // If the STL required std::random_device to be thread-safe and
67 // cryptographically-secure, this class could be made reasonably production
68 // ready by increasing use of entropy and making temporary file name selection
69 // thread-safe (in case a TempDir is static and shared across multiple threads).
70 //
71 // Today, this class does not provide much better safety guarantees than
72 // std::tmpnam(), but thanks to the required directory prefix and typical
73 // implementations of std::random_device, should see less risk of collisions in
74 // practice.
75 class TempDir {
76  public:
TempDir(std::string_view prefix)77   TempDir(std::string_view prefix) : rng_(GetSeed()) {
78     temp_dir_ = std::filesystem::temp_directory_path();
79     temp_dir_ /= std::string(prefix) + GetRandomSuffix();
80     PW_ASSERT(std::filesystem::create_directory(temp_dir_));
81   }
82 
~TempDir()83   ~TempDir() { PW_ASSERT(std::filesystem::remove_all(temp_dir_)); }
84 
GetTempFileName()85   std::filesystem::path GetTempFileName() {
86     return temp_dir_ / GetRandomSuffix();
87   }
88 
89  private:
GetRandomSuffix()90   std::string GetRandomSuffix() {
91     pw::StringBuffer<9> random_suffix_str;
92     uint32_t random_suffix_int = 0;
93     rng_.GetInt(random_suffix_int);
94     PW_ASSERT(random_suffix_str.Format("%08" PRIx32, random_suffix_int).ok());
95     return std::string(random_suffix_str.view());
96   }
97 
98   // Generate a 64-bit random from system entropy pool. This is used to seed a
99   // pseudo-random number generator for individual file names.
GetSeed()100   static uint64_t GetSeed() {
101     std::random_device sys_rand;
102     uint64_t seed = 0;
103     for (size_t seed_bytes = 0; seed_bytes < sizeof(seed);
104          seed_bytes += sizeof(std::random_device::result_type)) {
105       std::random_device::result_type val = sys_rand();
106       seed = seed << 8 * sizeof(std::random_device::result_type);
107       seed |= val;
108     }
109     return seed;
110   }
111 
112   random::XorShiftStarRng64 rng_;
113   std::filesystem::path temp_dir_;
114 };
115 
116 class StdFileStreamTest : public ::testing::Test {
117  protected:
118   StdFileStreamTest() = default;
119 
SetUp()120   void SetUp() override {
121     temp_file_path_ = temp_dir_.GetTempFileName().generic_string();
122   }
TearDown()123   void TearDown() override {
124     PW_ASSERT(std::filesystem::remove(TempFilename()));
125   }
126 
TempFilename()127   const char* TempFilename() { return temp_file_path_.c_str(); }
128 
129  private:
130   // Only construct one temporary directory to reduce waste of system entropy.
131   static TempDir temp_dir_;
132 
133   std::string temp_file_path_;
134 };
135 
136 TempDir StdFileStreamTest::temp_dir_{"StdFileStreamTest"};
137 
TEST_F(StdFileStreamTest,SeekAtEnd)138 TEST_F(StdFileStreamTest, SeekAtEnd) {
139   // Write some data to the temporary file.
140   const std::string_view kTestData = kSmallTestData;
141   StdFileWriter writer(TempFilename());
142   ASSERT_EQ(writer.Write(as_bytes(span(kTestData))), OkStatus());
143   writer.Close();
144 
145   StdFileReader reader(TempFilename());
146   ASSERT_EQ(reader.ConservativeReadLimit(), kTestData.size());
147 
148   std::array<char, 3> read_buffer;
149   size_t read_offset = 0;
150   while (read_offset < kTestData.size()) {
151     Result<ConstByteSpan> result =
152         reader.Read(as_writable_bytes(span(read_buffer)));
153     ASSERT_EQ(result.status(), OkStatus());
154     ASSERT_GT(result.value().size(), 0u);
155     ASSERT_LE(result.value().size(), read_buffer.size());
156     ASSERT_LE(result.value().size(), kTestData.size() - read_offset);
157     ConstByteSpan expect_window =
158         as_bytes(span(kTestData)).subspan(read_offset, result.value().size());
159     EXPECT_TRUE(pw::containers::Equal(result.value(), expect_window));
160     read_offset += result.value().size();
161     ASSERT_EQ(reader.ConservativeReadLimit(), kTestData.size() - read_offset);
162   }
163   // After data has been read, do a final read to trigger EOF.
164   Result<ConstByteSpan> result =
165       reader.Read(as_writable_bytes(span(read_buffer)));
166   EXPECT_EQ(result.status(), Status::OutOfRange());
167   ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
168 
169   EXPECT_EQ(read_offset, kTestData.size());
170 
171   // Seek backwards and read again to ensure seek at EOF works.
172   ASSERT_EQ(reader.Seek(-1 * read_buffer.size(), Stream::Whence::kEnd),
173             OkStatus());
174   ASSERT_EQ(reader.ConservativeReadLimit(), read_buffer.size());
175   result = reader.Read(as_writable_bytes(span(read_buffer)));
176   EXPECT_EQ(result.status(), OkStatus());
177 
178   reader.Close();
179 }
180 
181 }  // namespace
182 }  // namespace pw::stream
183