Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/scripts/write_github_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Write GITHUB_JSON from the environment to a file. Used by pr_targets workflow."""
import os
import sys


def main() -> None:
json_str = os.environ.get("GITHUB_JSON", "")
if not json_str:
sys.stderr.write("GITHUB_JSON environment variable is not set\n")
sys.exit(1)
if len(sys.argv) < 2:
sys.stderr.write("Usage: write_github_json.py <output_path>\n")
sys.exit(1)
path = sys.argv[1]
with open(path, "w") as f:
f.write(json_str)


if __name__ == "__main__":
main()
9 changes: 5 additions & 4 deletions .github/workflows/pr_targets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ jobs:

- name: compute targets
id: targes
env:
TRUNK_TOKEN: ${{ secrets.TRUNK_STAGING_ORG_API_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_JSON: ${{ toJSON(github) }}
run: |
echo "::group::GitHub Json"
TEMP_FILE=$(mktemp)
echo '${{ toJSON(github) }}' > $TEMP_FILE
python3 .github/scripts/write_github_json.py "$TEMP_FILE"
echo "::endgroup::"
cargo run -- upload-targets --github-json=$TEMP_FILE
env:
TRUNK_TOKEN: ${{ secrets.TRUNK_STAGING_ORG_API_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
117 changes: 94 additions & 23 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ use std::fs::OpenOptions;
use std::io::{BufRead, Write};
use std::io::{BufReader, BufWriter};

/// Parse a line as `{word}` or `{word} {integer}`.
/// Returns `Some((word, count))` where count is 0 when no integer is present.
/// Returns `None` if the line is not in that form (e.g. empty, or "word x y", or "word not_a_number").
fn parse_line(line: &str) -> Option<(String, u32)> {
let line = line.trim();
if line.is_empty() {
return None;
}
let parts: Vec<&str> = line.split_whitespace().collect();
match parts.len() {
1 => Some((parts[0].to_string(), 0)),
2 => parts[1]
.parse::<u32>()
.ok()
.map(|n| (parts[0].to_string(), n)),
_ => None,
}
}

pub fn change_file(filenames: &[String], count: u32) -> Vec<String> {
let mut rng = rand::thread_rng();
let mut words: Vec<String> = Vec::new();
Expand All @@ -12,25 +31,25 @@ pub fn change_file(filenames: &[String], count: u32) -> Vec<String> {
panic!("The count must be less than the number of files");
}

// Create a vector of indices and shuffle it to get unique random selections
let mut indices: Vec<usize> = (0..filenames.len()).collect();
for i in 0..count as usize {
let j = rng.gen_range(i..indices.len());
indices.swap(i, j);
}

// Take the first 'count' indices and process those files
for i in 0..count as usize {
let filename = &filenames[indices[i]];
words.push(move_random_line(filename));
words.push(edit_random_line(filename));
}

words
}

pub fn move_random_line(filename: &str) -> String {
// Read the file into a vector of lines
let file = std::fs::File::open(&filename).expect("Failed to open file");
/// Pick a random line in the file. If it matches `{word}` or `{word} {integer}`,
/// update it to `{word} {integer+1}`. If not, delete that line and try another until we edit one.
/// Returns the word that was edited.
fn edit_random_line(filename: &str) -> String {
let file = std::fs::File::open(filename).expect("Failed to open file");
let reader = BufReader::new(file);
let mut lines: Vec<String> = reader
.lines()
Expand All @@ -41,48 +60,100 @@ pub fn move_random_line(filename: &str) -> String {
panic!("Cannot continue the file {} is empty", filename);
}

// Choose a random line
let mut rng = rand::thread_rng();
let line_index = rng.gen_range(0..lines.len());

// Remove the line from the vector
let line = lines.remove(line_index);
let word = line.trim().to_string();
while !lines.is_empty() {
let line_index = rng.gen_range(0..lines.len());
let line = lines[line_index].clone();
let trimmed = line.trim();

if let Some((word, n)) = parse_line(trimmed) {
// Valid: replace with {word} {n+1}
lines[line_index] = format!("{} {}", word, n + 1);
write_lines(filename, &lines);
return word.to_lowercase();
}

// Choose another random line
let other_line_index = rng.gen_range(0..lines.len());
// Not in expected form: delete this line from the file
lines.remove(line_index);
write_lines(filename, &lines);
}

// Insert the line at the new position
lines.insert(other_line_index, line);
panic!(
"No valid line (format '{{word}}' or '{{word}} {{integer}}') left in file {}",
filename
);
}

// Write the lines back to the file
fn write_lines(filename: &str, lines: &[String]) {
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(&filename)
.expect("failed to open file");
.open(filename)
.expect("failed to open file for write");
let mut writer = BufWriter::new(file);
for line in lines {
writeln!(writer, "{}", line).expect("failed to write file");
}

word.to_lowercase().to_string()
}

/// Edit files for a PR based on the configuration and PR number.
/// Returns the words that were changed in the files.
pub fn edit_files_for_pr(filenames: &[String], pr_number: u32, config: &Conf) -> Vec<String> {
// Check if using new distribution approach or old approach
let (selected_files, change_count) = if config.pullrequest.deps_distribution.is_some() {
// New approach: use all available files, change dependency_count lines
let dependency_count = config.get_dependency_count(pr_number, filenames.len());
(filenames.to_vec(), dependency_count)
} else {
// Old approach: limit files to max_deps, change max_impacted_deps lines
let max_files = config.pullrequest.max_deps.min(filenames.len());
let files: Vec<String> = filenames.iter().take(max_files).cloned().collect();
(files, config.pullrequest.max_impacted_deps)
};

change_file(&selected_files, change_count as u32)
}

#[cfg(test)]
mod tests {
use super::parse_line;
use std::fs;

#[test]
fn test_parse_line_word_only() {
assert_eq!(parse_line("died"), Some(("died".into(), 0)));
assert_eq!(parse_line(" alpha "), Some(("alpha".into(), 0)));
}

#[test]
fn test_parse_line_word_and_integer() {
assert_eq!(parse_line("died 9"), Some(("died".into(), 9)));
assert_eq!(parse_line("died 1"), Some(("died".into(), 1)));
assert_eq!(parse_line(" word 42 "), Some(("word".into(), 42)));
}

#[test]
fn test_parse_line_invalid() {
assert_eq!(parse_line(""), None);
assert_eq!(parse_line(" "), None);
assert_eq!(parse_line("a b c"), None);
assert_eq!(parse_line("word abc"), None);
assert_eq!(parse_line("word -1"), None);
}

#[test]
fn test_edit_increments_and_writes() {
let dir = std::env::temp_dir().join("mq_edit_test");
fs::create_dir_all(&dir).unwrap();
let path = dir.join("f.txt");
fs::write(&path, "one\ntwo 3\nthree\n").unwrap();

let word = super::edit_random_line(path.to_str().unwrap());
let content = fs::read_to_string(&path).unwrap();
// One of the valid lines was edited: "one" -> "one 1", or "two 3" -> "two 4", or "three" -> "three 1"
assert!(
content.contains("one 1") || content.contains("two 4") || content.contains("three 1")
);
assert!(["one", "two", "three"].contains(&word.as_str()));

let _ = fs::remove_dir_all(&dir);
}
}