Enforce Rust and Swift ownership boundary
This commit is contained in:
@@ -80,6 +80,12 @@ pub struct IssueFilter {
|
||||
pub labels: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl IssueFilter {
|
||||
pub fn is_active(&self, status: &str) -> bool {
|
||||
status != "open" || self != &Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryData {
|
||||
pub owner: String,
|
||||
@@ -132,3 +138,21 @@ pub struct PullDetails {
|
||||
pub fn open_status() -> String {
|
||||
"open".into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn issue_filter_default_state_is_owned_by_rust() {
|
||||
assert!(!IssueFilter::default().is_active("open"));
|
||||
assert!(IssueFilter::default().is_active("closed"));
|
||||
assert!(
|
||||
IssueFilter {
|
||||
milestone: "v1".into(),
|
||||
..Default::default()
|
||||
}
|
||||
.is_active("open")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,26 @@ impl GotchaCore {
|
||||
Ok(issue_filter_options(&labels, &milestones, &filter))
|
||||
}
|
||||
|
||||
pub fn issue_filters_active(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
) -> Result<bool, GotchaError> {
|
||||
let server = self.server()?;
|
||||
let state = self.state.lock().unwrap();
|
||||
let filter = state
|
||||
.preferences
|
||||
.issue_filters
|
||||
.get(&repository_key(
|
||||
&server.url,
|
||||
owner.trim(),
|
||||
repository.trim(),
|
||||
))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Ok(filter.is_active(&state.preferences.issue_status))
|
||||
}
|
||||
|
||||
pub fn set_issue_filters(
|
||||
&self,
|
||||
owner: String,
|
||||
@@ -381,8 +401,9 @@ impl GotchaCore {
|
||||
owner: String,
|
||||
repository: String,
|
||||
path: String,
|
||||
) -> Result<Vec<u8>, GotchaError> {
|
||||
Ok(load_repository_file(&self.server()?, &owner, &repository, &path).await?)
|
||||
) -> Result<RepositoryFilePage, GotchaError> {
|
||||
let data = load_repository_file(&self.server()?, &owner, &repository, &path).await?;
|
||||
Ok(repository_file_page(&path, data))
|
||||
}
|
||||
|
||||
pub async fn commit_files(
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct IssuePage {
|
||||
pub struct IssueFilterOptions {
|
||||
pub milestones: Vec<String>,
|
||||
pub labels: Vec<String>,
|
||||
pub unavailable_labels: Vec<String>,
|
||||
pub selected_milestone: String,
|
||||
pub selected_labels: Vec<String>,
|
||||
}
|
||||
@@ -81,8 +82,9 @@ pub struct MilestoneRow {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub meta: String,
|
||||
pub open_issues: i64,
|
||||
pub closed_issues: i64,
|
||||
pub has_issues: bool,
|
||||
pub progress: f64,
|
||||
pub progress_accessibility: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -105,8 +107,7 @@ pub struct PullPage {
|
||||
pub struct CommitRow {
|
||||
pub sha: String,
|
||||
pub title: String,
|
||||
pub meta: String,
|
||||
pub refs: String,
|
||||
pub detail: String,
|
||||
pub top_lanes: Vec<u32>,
|
||||
pub bottom_lanes: Vec<u32>,
|
||||
pub node_lane: Option<u32>,
|
||||
@@ -134,6 +135,15 @@ pub struct RepositoryContentRow {
|
||||
pub size: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RepositoryFilePage {
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub language: String,
|
||||
pub text: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct DiffLine {
|
||||
pub old_number: String,
|
||||
@@ -164,17 +174,16 @@ pub struct ActivityRow {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct HeatCell {
|
||||
pub week: u32,
|
||||
pub day: u32,
|
||||
pub level: u32,
|
||||
pub timestamp: i64,
|
||||
pub contributions: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct HomePage {
|
||||
pub server_name: String,
|
||||
pub activities: Vec<ActivityRow>,
|
||||
pub issue_activities: Vec<ActivityRow>,
|
||||
pub pull_activities: Vec<ActivityRow>,
|
||||
pub heat_cells: Vec<HeatCell>,
|
||||
pub contribution_count: i64,
|
||||
}
|
||||
@@ -237,17 +246,28 @@ pub fn issue_filter_options(
|
||||
.iter()
|
||||
.filter_map(|label| label.name.clone())
|
||||
.collect();
|
||||
let unavailable_labels = filter
|
||||
.labels
|
||||
.iter()
|
||||
.filter(|label| !labels.contains(label))
|
||||
.cloned()
|
||||
.collect();
|
||||
labels.extend(filter.labels.iter().cloned());
|
||||
labels.sort_by_key(|label| label.to_lowercase());
|
||||
labels.dedup();
|
||||
let mut milestones: Vec<_> = milestones
|
||||
.iter()
|
||||
.filter_map(|milestone| milestone.title.clone())
|
||||
.collect();
|
||||
if !filter.milestone.is_empty() {
|
||||
milestones.push(filter.milestone.clone());
|
||||
}
|
||||
milestones.sort_by_key(|milestone| milestone.to_lowercase());
|
||||
milestones.dedup();
|
||||
IssueFilterOptions {
|
||||
milestones,
|
||||
labels,
|
||||
unavailable_labels,
|
||||
selected_milestone: filter.milestone.clone(),
|
||||
selected_labels: filter.labels.iter().cloned().collect(),
|
||||
}
|
||||
@@ -299,10 +319,12 @@ pub fn pull_rows(pulls: &[models::Issue]) -> Vec<PullRow> {
|
||||
number: pull.number.unwrap_or_default(),
|
||||
owner: repository.owner.clone()?,
|
||||
repository: repository.name.clone()?,
|
||||
title: pull
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Untitled pull request".into()),
|
||||
title: format!(
|
||||
"{} #{}\n{}",
|
||||
repository.name.as_deref()?,
|
||||
pull.number.unwrap_or_default(),
|
||||
pull.title.as_deref().unwrap_or("Untitled pull request")
|
||||
),
|
||||
summary: pull
|
||||
.body
|
||||
.as_deref()
|
||||
@@ -367,8 +389,16 @@ pub fn commit_page(
|
||||
.map(|message| message.lines().next().unwrap_or(message))
|
||||
.unwrap_or("Commit")
|
||||
.into(),
|
||||
meta: format!("{author} · {}", compact_date(date)),
|
||||
refs: row.refs.join(" · "),
|
||||
detail: if row.refs.is_empty() {
|
||||
format!("{author} · {}\n{}", compact_date(date), short_sha(sha))
|
||||
} else {
|
||||
format!(
|
||||
"{}\n{author} · {} · {}",
|
||||
row.refs.join(" · "),
|
||||
compact_date(date),
|
||||
short_sha(sha)
|
||||
)
|
||||
},
|
||||
top_lanes: indices(&row.top_lanes),
|
||||
bottom_lanes: indices(&row.bottom_lanes),
|
||||
node_lane: row.node_lane.map(|lane| lane as u32),
|
||||
@@ -488,6 +518,66 @@ pub fn repository_content_rows(
|
||||
rows
|
||||
}
|
||||
|
||||
pub fn repository_file_page(path: &str, data: Vec<u8>) -> RepositoryFilePage {
|
||||
let name = path.rsplit('/').next().unwrap_or("Preview").to_string();
|
||||
let extension = path.rsplit('.').next().unwrap_or_default().to_lowercase();
|
||||
let language = source_language(path).unwrap_or_default().to_string();
|
||||
let markdown = matches!(extension.as_str(), "md" | "markdown" | "mdown" | "mkd");
|
||||
let media = matches!(
|
||||
extension.as_str(),
|
||||
"apng"
|
||||
| "avi"
|
||||
| "bmp"
|
||||
| "flac"
|
||||
| "gif"
|
||||
| "heic"
|
||||
| "heif"
|
||||
| "jpeg"
|
||||
| "jpg"
|
||||
| "m4a"
|
||||
| "m4v"
|
||||
| "mkv"
|
||||
| "mov"
|
||||
| "mp3"
|
||||
| "mp4"
|
||||
| "mpeg"
|
||||
| "mpg"
|
||||
| "ogg"
|
||||
| "pdf"
|
||||
| "png"
|
||||
| "tif"
|
||||
| "tiff"
|
||||
| "wav"
|
||||
| "webm"
|
||||
| "webp"
|
||||
);
|
||||
if media {
|
||||
return RepositoryFilePage {
|
||||
name,
|
||||
kind: "preview".into(),
|
||||
language,
|
||||
text: String::new(),
|
||||
data,
|
||||
};
|
||||
}
|
||||
match String::from_utf8(data) {
|
||||
Ok(text) => RepositoryFilePage {
|
||||
name,
|
||||
kind: if markdown { "markdown" } else { "source" }.into(),
|
||||
language,
|
||||
text,
|
||||
data: Vec::new(),
|
||||
},
|
||||
Err(error) => RepositoryFilePage {
|
||||
name,
|
||||
kind: "preview".into(),
|
||||
language,
|
||||
text: String::new(),
|
||||
data: error.into_bytes(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage {
|
||||
DiffPage {
|
||||
title: path.rsplit('/').next().unwrap_or(path).into(),
|
||||
@@ -509,12 +599,51 @@ pub fn home_page(server_name: String, home: HomeData) -> HomePage {
|
||||
let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
|
||||
HomePage {
|
||||
server_name,
|
||||
issue_activities: home
|
||||
.activities
|
||||
.iter()
|
||||
.filter(|activity| is_issue_activity(activity))
|
||||
.map(activity_row)
|
||||
.collect(),
|
||||
pull_activities: home
|
||||
.activities
|
||||
.iter()
|
||||
.filter(|activity| is_pull_activity(activity))
|
||||
.map(activity_row)
|
||||
.collect(),
|
||||
activities: home.activities.iter().map(activity_row).collect(),
|
||||
heat_cells,
|
||||
contribution_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_issue_activity(activity: &models::Activity) -> bool {
|
||||
use models::activity::OpType;
|
||||
|
||||
matches!(
|
||||
activity.op_type,
|
||||
Some(OpType::CreateIssue | OpType::CloseIssue | OpType::ReopenIssue | OpType::CommentIssue)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_pull_activity(activity: &models::Activity) -> bool {
|
||||
use models::activity::OpType;
|
||||
|
||||
matches!(
|
||||
activity.op_type,
|
||||
Some(
|
||||
OpType::CreatePullRequest
|
||||
| OpType::MergePullRequest
|
||||
| OpType::AutoMergePullRequest
|
||||
| OpType::ClosePullRequest
|
||||
| OpType::ReopenPullRequest
|
||||
| OpType::CommentPull
|
||||
| OpType::ApprovePullRequest
|
||||
| OpType::RejectPullRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn activity_row(activity: &models::Activity) -> ActivityRow {
|
||||
use models::activity::OpType;
|
||||
|
||||
@@ -653,15 +782,20 @@ fn activity_detail(activity: &models::Activity) -> String {
|
||||
}
|
||||
|
||||
fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
||||
const DAYS: i64 = 9 * 31;
|
||||
let latest = data
|
||||
.iter()
|
||||
.filter_map(|entry| entry.timestamp)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
/ 86_400;
|
||||
let start = latest - DAYS + 1;
|
||||
let mut counts = vec![0_i64; DAYS as usize];
|
||||
let (year, month, _) = civil_from_days(latest);
|
||||
let first_month = year * 12 + month - 1 - 8;
|
||||
let start = days_from_civil(
|
||||
first_month.div_euclid(12),
|
||||
first_month.rem_euclid(12) + 1,
|
||||
1,
|
||||
);
|
||||
let mut counts = vec![0_i64; (latest - start + 1) as usize];
|
||||
for entry in data {
|
||||
let day = entry.timestamp.unwrap_or_default() / 86_400;
|
||||
if (start..=latest).contains(&day) {
|
||||
@@ -674,20 +808,42 @@ fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, count)| HeatCell {
|
||||
week: (index / 7) as u32,
|
||||
day: (index % 7) as u32,
|
||||
level: if count == 0 || maximum == 0 {
|
||||
0
|
||||
} else {
|
||||
((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
|
||||
},
|
||||
timestamp: (start + index as i64) * 86_400,
|
||||
contributions: count,
|
||||
})
|
||||
.collect();
|
||||
(cells, contribution_count)
|
||||
}
|
||||
|
||||
fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
||||
let days = days + 719_468;
|
||||
let era = days.div_euclid(146_097);
|
||||
let day_of_era = days - era * 146_097;
|
||||
let year_of_era =
|
||||
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
||||
let mut year = year_of_era + era * 400;
|
||||
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
||||
let month_prime = (5 * day_of_year + 2) / 153;
|
||||
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
|
||||
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
|
||||
year += i64::from(month <= 2);
|
||||
(year, month, day)
|
||||
}
|
||||
|
||||
fn days_from_civil(mut year: i64, month: i64, day: i64) -> i64 {
|
||||
year -= i64::from(month <= 2);
|
||||
let era = year.div_euclid(400);
|
||||
let year_of_era = year - era * 400;
|
||||
let month_prime = month + if month > 2 { -3 } else { 9 };
|
||||
let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
|
||||
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
|
||||
era * 146_097 + day_of_era - 719_468
|
||||
}
|
||||
|
||||
fn label_row(label: &models::Label) -> Option<LabelRow> {
|
||||
let name = label.name.as_deref()?.trim();
|
||||
let color = label.color.as_deref()?.trim().trim_start_matches('#');
|
||||
@@ -777,6 +933,7 @@ fn issue_milestone(issue: &models::Issue) -> String {
|
||||
fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
|
||||
let open = milestone.open_issues.unwrap_or_default();
|
||||
let closed = milestone.closed_issues.unwrap_or_default();
|
||||
let total = open + closed;
|
||||
let due = milestone
|
||||
.due_on
|
||||
.as_deref()
|
||||
@@ -798,8 +955,68 @@ fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
|
||||
milestone.state.as_deref().unwrap_or("unknown"),
|
||||
open + closed
|
||||
),
|
||||
open_issues: open,
|
||||
closed_issues: closed,
|
||||
has_issues: total > 0,
|
||||
progress: if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
closed as f64 / total as f64
|
||||
},
|
||||
progress_accessibility: format!("{closed} closed, {open} open"),
|
||||
}
|
||||
}
|
||||
|
||||
fn short_sha(sha: &str) -> &str {
|
||||
&sha[..sha.len().min(8)]
|
||||
}
|
||||
|
||||
fn source_language(path: &str) -> Option<&'static str> {
|
||||
let filename = path.rsplit('/').next()?.to_lowercase();
|
||||
match filename.as_str() {
|
||||
"cmakelists.txt" => return Some("cmake"),
|
||||
"dockerfile" => return Some("dockerfile"),
|
||||
"gemfile" | "podfile" => return Some("ruby"),
|
||||
"makefile" => return Some("makefile"),
|
||||
_ => {}
|
||||
}
|
||||
match filename.rsplit('.').next()? {
|
||||
"asm" => Some("x86asm"),
|
||||
"c" => Some("c"),
|
||||
"cc" | "cpp" | "h" | "hpp" => Some("cpp"),
|
||||
"clj" => Some("clojure"),
|
||||
"cs" => Some("csharp"),
|
||||
"css" => Some("css"),
|
||||
"dart" => Some("dart"),
|
||||
"ex" | "exs" => Some("elixir"),
|
||||
"fs" => Some("fsharp"),
|
||||
"go" => Some("go"),
|
||||
"groovy" => Some("groovy"),
|
||||
"hs" => Some("haskell"),
|
||||
"html" | "plist" | "xml" => Some("xml"),
|
||||
"java" => Some("java"),
|
||||
"jl" => Some("julia"),
|
||||
"js" | "jsx" => Some("javascript"),
|
||||
"json" => Some("json"),
|
||||
"kt" | "kts" => Some("kotlin"),
|
||||
"lua" => Some("lua"),
|
||||
"m" | "mm" => Some("objectivec"),
|
||||
"md" | "markdown" | "mdown" | "mkd" => Some("markdown"),
|
||||
"php" => Some("php"),
|
||||
"pl" => Some("perl"),
|
||||
"ps1" => Some("powershell"),
|
||||
"py" => Some("python"),
|
||||
"r" => Some("r"),
|
||||
"rb" => Some("ruby"),
|
||||
"rs" => Some("rust"),
|
||||
"scala" => Some("scala"),
|
||||
"scss" => Some("scss"),
|
||||
"sh" => Some("bash"),
|
||||
"sql" => Some("sql"),
|
||||
"swift" => Some("swift"),
|
||||
"toml" => Some("ini"),
|
||||
"ts" | "tsx" => Some("typescript"),
|
||||
"txt" => Some("plaintext"),
|
||||
"yaml" | "yml" => Some("yaml"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,24 +1070,19 @@ mod tests {
|
||||
fn maps_heat_levels_and_labels() {
|
||||
let heatmap = vec![
|
||||
models::UserHeatmapData {
|
||||
timestamp: Some(100 * 86_400),
|
||||
timestamp: Some(20_393 * 86_400),
|
||||
contributions: Some(1),
|
||||
},
|
||||
models::UserHeatmapData {
|
||||
timestamp: Some(101 * 86_400),
|
||||
timestamp: Some(20_665 * 86_400),
|
||||
contributions: Some(4),
|
||||
},
|
||||
];
|
||||
let (cells, total) = heat_cells(&heatmap);
|
||||
assert_eq!((cells.len(), total), (279, 5));
|
||||
assert_eq!((cells.len(), total), (273, 5));
|
||||
assert_eq!(
|
||||
(
|
||||
cells[277].level,
|
||||
cells[277].timestamp,
|
||||
cells[277].contributions,
|
||||
cells[278].level,
|
||||
),
|
||||
(1, 100 * 86_400, 1, 4)
|
||||
(cells[0].level, cells[0].timestamp, cells[272].level,),
|
||||
(1, 20_393 * 86_400, 4)
|
||||
);
|
||||
|
||||
let label = models::Label {
|
||||
@@ -890,10 +1102,8 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0);
|
||||
assert_eq!(
|
||||
(milestone_row.open_issues, milestone_row.closed_issues),
|
||||
(3, 2)
|
||||
);
|
||||
assert_eq!(milestone_row.progress, 0.4);
|
||||
assert_eq!(milestone_row.progress_accessibility, "2 closed, 3 open");
|
||||
let issue = models::Issue {
|
||||
milestone: Some(Box::new(milestone)),
|
||||
..Default::default()
|
||||
@@ -901,6 +1111,33 @@ mod tests {
|
||||
assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn categorizes_home_activity_before_crossing_the_bridge() {
|
||||
use models::activity::OpType;
|
||||
|
||||
let page = home_page(
|
||||
"Gitea".into(),
|
||||
HomeData {
|
||||
activities: [
|
||||
OpType::CreateIssue,
|
||||
OpType::CreatePullRequest,
|
||||
OpType::CreateRepo,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|op_type| models::Activity {
|
||||
op_type: Some(op_type),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
heatmap: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(page.activities.len(), 3);
|
||||
assert_eq!(page.issue_activities.len(), 1);
|
||||
assert_eq!(page.pull_activities.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_page_exposes_state() {
|
||||
let page = issue_page(IssueDetails {
|
||||
@@ -917,8 +1154,8 @@ mod tests {
|
||||
#[test]
|
||||
fn builds_sorted_issue_filter_options() {
|
||||
let filter = IssueFilter {
|
||||
milestone: "v2".into(),
|
||||
labels: ["bug".into(), "critical".into()].into(),
|
||||
milestone: "v3".into(),
|
||||
labels: ["bug".into(), "retired".into()].into(),
|
||||
};
|
||||
let options = issue_filter_options(
|
||||
&[
|
||||
@@ -944,9 +1181,38 @@ mod tests {
|
||||
&filter,
|
||||
);
|
||||
|
||||
assert_eq!(options.labels, ["bug", "critical"]);
|
||||
assert_eq!(options.milestones, ["v1", "v2"]);
|
||||
assert_eq!(options.selected_labels, ["bug", "critical"]);
|
||||
assert_eq!(options.selected_milestone, "v2");
|
||||
assert_eq!(options.labels, ["bug", "critical", "retired"]);
|
||||
assert_eq!(options.unavailable_labels, ["retired"]);
|
||||
assert_eq!(options.milestones, ["v1", "v2", "v3"]);
|
||||
assert_eq!(options.selected_labels, ["bug", "retired"]);
|
||||
assert_eq!(options.selected_milestone, "v3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_repository_files_in_rust() {
|
||||
let markdown = repository_file_page("docs/README.md", b"# Hello".to_vec());
|
||||
assert_eq!(
|
||||
(
|
||||
markdown.name.as_str(),
|
||||
markdown.kind.as_str(),
|
||||
markdown.language.as_str()
|
||||
),
|
||||
("README.md", "markdown", "markdown")
|
||||
);
|
||||
assert_eq!(markdown.text, "# Hello");
|
||||
|
||||
let source = repository_file_page("Sources/App.swift", b"import UIKit".to_vec());
|
||||
assert_eq!(
|
||||
(source.kind.as_str(), source.language.as_str()),
|
||||
("source", "swift")
|
||||
);
|
||||
|
||||
let preview = repository_file_page("image.png", vec![0, 159]);
|
||||
assert_eq!(preview.kind, "preview");
|
||||
assert_eq!(preview.data, [0, 159]);
|
||||
|
||||
for path in ["manual.pdf", "sound.mp3", "movie.mp4"] {
|
||||
assert_eq!(repository_file_page(path, Vec::new()).kind, "preview");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user