• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MilesCranmer / rip2 / #472

22 Oct 2024 10:37PM UTC coverage: 71.364% (-2.9%) from 74.306%
#472

Pull #60

MilesCranmer
refactor: cache some env vars
Pull Request #60: Speedup: cache env var

6 of 14 new or added lines in 2 files covered. (42.86%)

19 existing lines in 2 files now uncovered.

314 of 440 relevant lines covered (71.36%)

1503238552.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.31
/src/record.rs
1
use chrono::Local;
2
use fs4::fs_std::FileExt;
3
use std::io::{BufRead, BufReader, Error, ErrorKind, Read, Write};
4
use std::path::{Path, PathBuf};
5
use std::{fs, io};
6

7
use crate::util;
8

9
pub const RECORD: &str = ".record";
10

11
#[derive(Debug)]
12
pub struct RecordItem {
13
    pub time: String,
14
    pub orig: PathBuf,
15
    pub dest: PathBuf,
16
}
17

18
impl RecordItem {
19
    /// Parse a line in the record into a `RecordItem`
20
    pub fn new(line: &str) -> RecordItem {
2,147,483,647✔
21
        let mut tokens = line.split('\t');
2,147,483,647✔
22
        let time = tokens.next().expect("Bad format: column 1").to_string();
2,147,483,647✔
23
        let orig = tokens.next().expect("Bad format: column 2").to_string();
2,147,483,647✔
24
        let dest = tokens.next().expect("Bad format: column 3").to_string();
2,147,483,647✔
25
        RecordItem {
26
            time,
27
            orig: PathBuf::from(orig),
2,147,483,647✔
28
            dest: PathBuf::from(dest),
2,147,483,647✔
29
        }
30
    }
31
}
32

33
/// A record of file operations maintained in the graveyard directory
34
///
35
/// # Type Parameters
36
///
37
/// * `FILE_LOCK` - When `true`, exclusive file locks are acquired when opening
38
///   the record file for reading or writing. This prevents concurrent access from multiple
39
///   processes. When `false`, no file locking is performed - which is used for testing.
40
#[derive(Debug)]
41
pub struct Record<const FILE_LOCK: bool> {
42
    path: PathBuf,
43
}
44

45
#[cfg(not(target_os = "windows"))]
46
pub const DEFAULT_FILE_LOCK: bool = true;
47

48
#[cfg(target_os = "windows")]
49
pub const DEFAULT_FILE_LOCK: bool = false;
50
// TODO: Investigate why this is needed. Does Windows not support file locks?
51

52
impl<const FILE_LOCK: bool> Record<FILE_LOCK> {
53
    pub fn new(graveyard: &Path) -> Record<FILE_LOCK> {
2,147,483,647✔
54
        let path = graveyard.join(RECORD);
2,147,483,647✔
55
        // Create the record file if it doesn't exist
56
        if !path.exists() {
2,147,483,647✔
57
            // Write a header to the record file
58
            let mut record_file = fs::OpenOptions::new()
2,147,483,647✔
59
                .truncate(true)
60
                .create(true)
61
                .write(true)
62
                .open(&path)
2,147,483,647✔
63
                .expect("Failed to open record file");
64
            if FILE_LOCK {
2,147,483,647✔
UNCOV
65
                record_file.lock_exclusive().unwrap();
×
66
            }
67
            record_file
2,147,483,647✔
68
                .write_all(b"Time\tOriginal\tDestination\n")
69
                .expect("Failed to write header to record file");
70
        }
71
        Record { path }
72
    }
73

74
    pub fn open(&self) -> Result<fs::File, Error> {
2,147,483,647✔
75
        let file = fs::File::open(&self.path)
2,147,483,647✔
76
            .map_err(|_| Error::new(ErrorKind::NotFound, "Failed to read record!"))?;
2,147,483,647✔
UNCOV
77
        if FILE_LOCK {
×
UNCOV
78
            file.lock_exclusive().unwrap();
×
79
        }
80
        Ok(file)
2,147,483,647✔
81
    }
82

83
    /// Return the path in the graveyard of the last file to be buried.
84
    /// As a side effect, any valid last files that are found in the record but
85
    /// not on the filesystem are removed from the record.
86
    pub fn get_last_bury(&self) -> Result<PathBuf, Error> {
2,147,483,647✔
87
        // record: impl AsRef<Path>
88
        let record_file = self.open()?;
2,147,483,647✔
89
        let mut contents = String::new();
×
90
        BufReader::new(&record_file).read_to_string(&mut contents)?;
×
91

92
        // This will be None if there is nothing, or Some
93
        // if there is items in the vector
94
        let mut graves_to_exhume: Vec<PathBuf> = Vec::new();
2,147,483,647✔
95
        let mut lines = contents.lines();
2,147,483,647✔
96
        lines.next();
2,147,483,647✔
97
        for entry in lines.rev().map(RecordItem::new) {
2,147,483,647✔
98
            // Check that the file is still in the graveyard.
99
            // If it is, return the corresponding line.
100
            if util::symlink_exists(&entry.dest) {
2,147,483,647✔
101
                if !graves_to_exhume.is_empty() {
2,147,483,647✔
102
                    self.delete_lines(record_file, &graves_to_exhume)?;
×
103
                }
104
                return Ok(entry.dest);
2,147,483,647✔
105
            } else {
106
                // File is gone, mark the grave to be removed from the record
107
                graves_to_exhume.push(entry.dest);
×
108
            }
109
        }
110

111
        if !graves_to_exhume.is_empty() {
×
112
            self.delete_lines(record_file, &graves_to_exhume)?;
×
113
        }
114
        Err(Error::new(ErrorKind::NotFound, "No files in graveyard"))
×
115
    }
116

117
    /// Takes a vector of grave paths and removes the respective lines from the record
118
    fn delete_lines(&self, record_file: fs::File, graves: &[PathBuf]) -> Result<(), Error> {
2,147,483,647✔
119
        let record_path = &self.path;
2,147,483,647✔
120
        // Get the lines to write back to the record, which is every line except
121
        // the ones matching the exhumed graves. Store them in a vector
122
        // since we'll be overwriting the record in-place.
123
        let mut reader = BufReader::new(record_file).lines();
2,147,483,647✔
124
        let header = reader
2,147,483,647✔
125
            .next()
126
            .unwrap_or_else(|| Ok(String::new()))
2,147,483,647✔
127
            .unwrap_or_default(); // Capture the header
128
        let lines_to_write: Vec<String> = reader
2,147,483,647✔
129
            .map_while(Result::ok)
2,147,483,647✔
130
            .filter(|line| !graves.iter().any(|y| *y == RecordItem::new(line).dest))
2,147,483,647✔
131
            .collect();
132
        // let mut new_record_file = fs::File::create(record_path)?;
133
        let mut new_record_file = fs::OpenOptions::new()
2,147,483,647✔
134
            .create(true)
135
            .truncate(true)
136
            .write(true)
137
            .open(record_path)?;
2,147,483,647✔
UNCOV
138
        if FILE_LOCK {
×
UNCOV
139
            new_record_file.lock_exclusive().unwrap();
×
140
        }
141
        writeln!(new_record_file, "{}", header)?; // Write the header back
×
142
        for line in lines_to_write {
2,147,483,647✔
143
            writeln!(new_record_file, "{}", line)?;
×
144
        }
145
        Ok(())
2,147,483,647✔
146
    }
147

148
    pub fn log_exhumed_graves(&self, graves_to_exhume: &[PathBuf]) -> Result<(), Error> {
2,147,483,647✔
149
        // Reopen the record and then delete lines corresponding to exhumed graves
150
        let record_file = self.open()?;
2,147,483,647✔
151
        self.delete_lines(record_file, graves_to_exhume)
×
152
            .map_err(|e| {
×
153
                Error::new(
×
154
                    e.kind(),
×
155
                    format!("Failed to remove unburied files from record: {}", e),
×
156
                )
157
            })
158
    }
159

160
    /// Takes a vector of grave paths and returns the respective lines in the record
161
    pub fn lines_of_graves<'a>(
2,147,483,647✔
162
        &'a self,
163
        graves: &'a [PathBuf],
164
    ) -> impl Iterator<Item = String> + 'a {
165
        let record_file = self.open().unwrap();
2,147,483,647✔
166
        let mut reader = BufReader::new(record_file).lines();
2,147,483,647✔
167
        reader.next();
2,147,483,647✔
168
        reader
2,147,483,647✔
169
            .map_while(Result::ok)
2,147,483,647✔
170
            .filter(move |line| graves.iter().any(|y| *y == RecordItem::new(line).dest))
2,147,483,647✔
171
    }
172

173
    /// Returns an iterator over all graves in the record that are under gravepath
174
    pub fn seance<'a>(
2,147,483,647✔
175
        &'a self,
176
        gravepath: &'a PathBuf,
177
    ) -> io::Result<impl Iterator<Item = RecordItem> + 'a> {
178
        let record_file = self.open()?;
2,147,483,647✔
179
        let mut reader = BufReader::new(record_file).lines();
×
180
        reader.next();
×
181
        Ok(reader
×
182
            .map_while(Result::ok)
×
183
            .map(|line| RecordItem::new(&line))
2,147,483,647✔
184
            .filter(move |record_item| record_item.dest.starts_with(gravepath)))
2,147,483,647✔
185
    }
186

187
    /// Write deletion history to record
188
    pub fn write_log(&self, source: impl AsRef<Path>, dest: impl AsRef<Path>) -> io::Result<()> {
2,147,483,647✔
189
        let (source, dest) = (source.as_ref(), dest.as_ref());
2,147,483,647✔
190

191
        let already_existed = self.path.exists();
2,147,483,647✔
192

193
        // TODO: The tiny amount of time between the check and the open
194
        //       could allow for a race condition. But maybe I'm being overkill.
195

196
        let mut record_file = if already_existed {
2,147,483,647✔
197
            fs::OpenOptions::new().append(true).open(&self.path)?
2,147,483,647✔
198
        } else {
199
            fs::OpenOptions::new()
×
200
                .create(true)
201
                .truncate(true)
202
                .write(true)
203
                .open(&self.path)?
×
204
        };
205

UNCOV
206
        if FILE_LOCK {
×
UNCOV
207
            record_file.lock_exclusive().unwrap();
×
208
        }
209

210
        if !already_existed {
×
211
            writeln!(record_file, "Time\tOriginal\tDestination")?;
×
212
        }
213

214
        writeln!(
2,147,483,647✔
215
            record_file,
2,147,483,647✔
216
            "{}\t{}\t{}",
217
            Local::now().to_rfc3339(),
2,147,483,647✔
218
            source.display(),
2,147,483,647✔
219
            dest.display()
2,147,483,647✔
220
        )
221
        .map_err(|e| {
2,147,483,647✔
222
            Error::new(
×
223
                e.kind(),
×
224
                format!("Failed to write record at {}", &self.path.display()),
×
225
            )
226
        })?;
227

228
        Ok(())
2,147,483,647✔
229
    }
230
}
231

232
impl<const FILE_LOCK: bool> Clone for Record<FILE_LOCK> {
UNCOV
233
    fn clone(&self) -> Self {
×
234
        Record {
UNCOV
235
            path: self.path.clone(),
×
236
        }
237
    }
238
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc