mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 14:52:01 +00:00
Use insta_cmd (#6737)
This commit is contained in:
parent
7ead2c17b1
commit
e02d76f070
6 changed files with 423 additions and 311 deletions
|
@ -8,14 +8,15 @@ use std::fs::Permissions;
|
|||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
#[cfg(unix)]
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
|
||||
#[cfg(unix)]
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use assert_cmd::Command;
|
||||
#[cfg(unix)]
|
||||
use clap::Parser;
|
||||
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
|
||||
#[cfg(unix)]
|
||||
use path_absolutize::path_dedot;
|
||||
#[cfg(unix)]
|
||||
|
@ -27,315 +28,280 @@ use ruff_cli::args::Args;
|
|||
use ruff_cli::run;
|
||||
|
||||
const BIN_NAME: &str = "ruff";
|
||||
const STDIN_BASE_OPTIONS: &[&str] = &["--isolated", "--no-cache", "-", "--format", "text"];
|
||||
|
||||
#[test]
|
||||
fn stdin_success() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
cmd.args(["-", "--format", "text", "--isolated"])
|
||||
.write_stdin("")
|
||||
.assert()
|
||||
.success();
|
||||
Ok(())
|
||||
fn stdin_success() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.pass_stdin(""), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_error() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--isolated"])
|
||||
.write_stdin("import os\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
r#"-:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
"#
|
||||
);
|
||||
Ok(())
|
||||
fn stdin_error() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.pass_stdin("import os\n"), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_filename() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args([
|
||||
"-",
|
||||
"--format",
|
||||
"text",
|
||||
"--stdin-filename",
|
||||
"F401.py",
|
||||
"--isolated",
|
||||
])
|
||||
.write_stdin("import os\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
r#"F401.py:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
"#
|
||||
);
|
||||
Ok(())
|
||||
fn stdin_filename() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--stdin-filename", "F401.py"])
|
||||
.pass_stdin("import os\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
F401.py:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_source_type() -> Result<()> {
|
||||
// Raise `TCH` errors in `.py` files.
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args([
|
||||
"-",
|
||||
"--format",
|
||||
"text",
|
||||
"--stdin-filename",
|
||||
"TCH.py",
|
||||
"--isolated",
|
||||
])
|
||||
.write_stdin("import os\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
r#"TCH.py:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
"#
|
||||
);
|
||||
/// Raise `TCH` errors in `.py` files ...
|
||||
fn stdin_source_type_py() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--stdin-filename", "TCH.py"])
|
||||
.pass_stdin("import os\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
TCH.py:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
|
||||
// But not in `.pyi` files.
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
cmd.args([
|
||||
"-",
|
||||
"--format",
|
||||
"text",
|
||||
"--stdin-filename",
|
||||
"TCH.pyi",
|
||||
"--isolated",
|
||||
"--select",
|
||||
"TCH",
|
||||
])
|
||||
.write_stdin("import os\n")
|
||||
.assert()
|
||||
.success();
|
||||
Ok(())
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
/// ... but not in `.pyi` files.
|
||||
#[test]
|
||||
fn stdin_source_type_pyi() {
|
||||
let args = ["--stdin-filename", "TCH.pyi", "--select", "TCH"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("import os\n"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn stdin_json() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args([
|
||||
"-",
|
||||
"--format",
|
||||
"json",
|
||||
"--stdin-filename",
|
||||
"F401.py",
|
||||
"--isolated",
|
||||
])
|
||||
.write_stdin("import os\n")
|
||||
.assert()
|
||||
.failure();
|
||||
fn stdin_json() {
|
||||
let args = [
|
||||
"-",
|
||||
"--isolated",
|
||||
"--no-cache",
|
||||
"--format",
|
||||
"json",
|
||||
"--stdin-filename",
|
||||
"F401.py",
|
||||
];
|
||||
|
||||
let directory = path_dedot::CWD.to_str().unwrap();
|
||||
let binding = Path::new(directory).join("F401.py");
|
||||
let file_path = binding.display();
|
||||
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
format!(
|
||||
r#"[
|
||||
{{
|
||||
"code": "F401",
|
||||
"end_location": {{
|
||||
"column": 10,
|
||||
"row": 1
|
||||
}},
|
||||
"filename": "{file_path}",
|
||||
"fix": {{
|
||||
"applicability": "Automatic",
|
||||
"edits": [
|
||||
{{
|
||||
"content": "",
|
||||
"end_location": {{
|
||||
"column": 1,
|
||||
"row": 2
|
||||
}},
|
||||
"location": {{
|
||||
"column": 1,
|
||||
"row": 1
|
||||
}}
|
||||
}}
|
||||
],
|
||||
"message": "Remove unused import: `os`"
|
||||
}},
|
||||
"location": {{
|
||||
"column": 8,
|
||||
"row": 1
|
||||
}},
|
||||
"message": "`os` imported but unused",
|
||||
"noqa_row": 1,
|
||||
"url": "https://beta.ruff.rs/docs/rules/unused-import"
|
||||
}}
|
||||
]"#
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
insta::with_settings!({filters => vec![
|
||||
(file_path.to_string().as_str(), "/path/to/F401.py"),
|
||||
]}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(args)
|
||||
.pass_stdin("import os\n"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_autofix() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--fix", "--isolated"])
|
||||
.write_stdin("import os\nimport sys\n\nprint(sys.version)\n")
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
"import sys\n\nprint(sys.version)\n"
|
||||
);
|
||||
Ok(())
|
||||
fn stdin_autofix() {
|
||||
let args = ["--fix"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("import os\nimport sys\n\nprint(sys.version)\n"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
import sys
|
||||
|
||||
print(sys.version)
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_autofix_when_not_fixable_should_still_print_contents() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--fix", "--isolated"])
|
||||
.write_stdin("import os\nimport sys\n\nif (1, 2):\n print(sys.version)\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
"import sys\n\nif (1, 2):\n print(sys.version)\n"
|
||||
);
|
||||
Ok(())
|
||||
fn stdin_autofix_when_not_fixable_should_still_print_contents() {
|
||||
let args = ["--fix"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("import os\nimport sys\n\nif (1, 2):\n print(sys.version)\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
import sys
|
||||
|
||||
if (1, 2):
|
||||
print(sys.version)
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_autofix_when_no_issues_should_still_print_contents() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--fix", "--isolated"])
|
||||
.write_stdin("import sys\n\nprint(sys.version)\n")
|
||||
.assert()
|
||||
.success();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
"import sys\n\nprint(sys.version)\n"
|
||||
);
|
||||
Ok(())
|
||||
fn stdin_autofix_when_no_issues_should_still_print_contents() {
|
||||
let args = ["--fix"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("import sys\n\nprint(sys.version)\n"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
import sys
|
||||
|
||||
print(sys.version)
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_source() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--show-source", "--isolated"])
|
||||
.write_stdin("l = 1")
|
||||
.assert()
|
||||
.failure();
|
||||
assert!(str::from_utf8(&output.get_output().stdout)?.contains("l = 1"));
|
||||
Ok(())
|
||||
fn show_source() {
|
||||
let args = ["--show-source"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("l = 1"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
|
|
||||
1 | l = 1
|
||||
| ^ E741
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explain_status_codes() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
cmd.args(["--explain", "F401"]).assert().success();
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
cmd.args(["--explain", "RUF404"]).assert().failure();
|
||||
Ok(())
|
||||
fn explain_status_codes_f401() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)).args(["--explain", "F401"]));
|
||||
}
|
||||
#[test]
|
||||
fn explain_status_codes_ruf404() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)).args(["--explain", "RUF404"]), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: invalid value 'RUF404' for '[RULE]': unknown rule code
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_statistics() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
.args([
|
||||
"-",
|
||||
"--format",
|
||||
"text",
|
||||
"--select",
|
||||
"F401",
|
||||
"--statistics",
|
||||
"--isolated",
|
||||
])
|
||||
.write_stdin("import sys\nimport os\n\nprint(os.getuid())\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?
|
||||
.lines()
|
||||
.last()
|
||||
.unwrap(),
|
||||
"1\tF401\t[*] `sys` imported but unused"
|
||||
);
|
||||
Ok(())
|
||||
fn show_statistics() {
|
||||
let args = ["--select", "F401", "--statistics"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("import sys\nimport os\n\nprint(os.getuid())\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
1 F401 [*] `sys` imported but unused
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nursery_prefix() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
|
||||
fn nursery_prefix() {
|
||||
// `--select E` should detect E741, but not E225, which is in the nursery.
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--isolated", "--select", "E"])
|
||||
.write_stdin("I=42\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
r#"-:1:1: E741 Ambiguous variable name: `I`
|
||||
Found 1 error.
|
||||
"#
|
||||
);
|
||||
let args = ["--select", "E"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `I`
|
||||
Found 1 error.
|
||||
|
||||
Ok(())
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nursery_all() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
|
||||
fn nursery_all() {
|
||||
// `--select ALL` should detect E741, but not E225, which is in the nursery.
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--isolated", "--select", "E"])
|
||||
.write_stdin("I=42\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
r#"-:1:1: E741 Ambiguous variable name: `I`
|
||||
Found 1 error.
|
||||
"#
|
||||
);
|
||||
let args = ["--select", "ALL"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `I`
|
||||
-:1:1: D100 Missing docstring in public module
|
||||
Found 2 errors.
|
||||
|
||||
Ok(())
|
||||
----- stderr -----
|
||||
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
|
||||
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nursery_direct() -> Result<()> {
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
|
||||
fn nursery_direct() {
|
||||
// `--select E225` should detect E225.
|
||||
let output = cmd
|
||||
.args(["-", "--format", "text", "--isolated", "--select", "E225"])
|
||||
.write_stdin("I=42\n")
|
||||
.assert()
|
||||
.failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
r#"-:1:2: E225 Missing whitespace around operator
|
||||
Found 1 error.
|
||||
"#
|
||||
);
|
||||
let args = ["--select", "E225"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 1 error.
|
||||
|
||||
Ok(())
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
/// An unreadable pyproject.toml in non-isolated mode causes ruff to hard-error trying to build up
|
||||
|
@ -376,17 +342,17 @@ fn unreadable_dir() -> Result<()> {
|
|||
|
||||
// We (currently?) have to use a subcommand to check exit status (currently wrong) and logging
|
||||
// output
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd
|
||||
// TODO(konstin): This should be a failure, but we currently can't track that
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["--no-cache", "--isolated"])
|
||||
.arg(&unreadable_dir)
|
||||
.assert()
|
||||
// TODO(konstin): This should be a failure, but we currently can't track that
|
||||
.success();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stderr)?,
|
||||
"warning: Encountered error: Permission denied (os error 13)\n"
|
||||
);
|
||||
.arg(&unreadable_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Encountered error: Permission denied (os error 13)
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -416,17 +382,22 @@ fn check_input_from_argfile() -> Result<()> {
|
|||
format!("@{}", &input_file_path.display()),
|
||||
];
|
||||
|
||||
let mut cmd = Command::cargo_bin(BIN_NAME)?;
|
||||
let output = cmd.args(args).write_stdin("").assert().failure();
|
||||
assert_eq!(
|
||||
str::from_utf8(&output.get_output().stdout)?,
|
||||
format!(
|
||||
"{}:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
",
|
||||
file_a_path.display()
|
||||
)
|
||||
);
|
||||
insta::with_settings!({filters => vec![
|
||||
(file_a_path.display().to_string().as_str(), "/path/to/a.py"),
|
||||
]}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(args)
|
||||
.pass_stdin(""), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
/path/to/a.py:1:8: F401 [*] `os` imported but unused
|
||||
Found 1 error.
|
||||
[*] 1 potentially fixable with the --fix option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue