tests_candidates.rs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. //! Tests for `candidate_reviewers_from_names`
  2. use super::super::*;
  3. /// Basic test function for testing `candidate_reviewers_from_names`.
  4. fn test_from_names(
  5. teams: Option<toml::Table>,
  6. config: toml::Table,
  7. issue: serde_json::Value,
  8. names: &[&str],
  9. expected: Result<&[&str], FindReviewerError>,
  10. ) {
  11. let (teams, config, issue) = convert_simplified(teams, config, issue);
  12. let names: Vec<_> = names.iter().map(|n| n.to_string()).collect();
  13. match (
  14. candidate_reviewers_from_names(&teams, &config, &issue, &names),
  15. expected,
  16. ) {
  17. (Ok(candidates), Ok(expected)) => {
  18. let mut candidates: Vec<_> = candidates.into_iter().collect();
  19. candidates.sort();
  20. let expected: Vec<_> = expected.iter().map(|x| *x).collect();
  21. assert_eq!(candidates, expected);
  22. }
  23. (Err(actual), Err(expected)) => {
  24. assert_eq!(actual, expected)
  25. }
  26. (Ok(candidates), Err(_)) => panic!("expected Err, got Ok: {candidates:?}"),
  27. (Err(e), Ok(_)) => panic!("expected Ok, got Err: {e}"),
  28. }
  29. }
  30. /// Convert the simplified input in preparation for `candidate_reviewers_from_names`.
  31. fn convert_simplified(
  32. teams: Option<toml::Table>,
  33. config: toml::Table,
  34. issue: serde_json::Value,
  35. ) -> (Teams, AssignConfig, Issue) {
  36. // Convert the simplified team config to a real team config.
  37. // This uses serde_json since it is easier to manipulate than toml.
  38. let teams: serde_json::Value = match teams {
  39. Some(teams) => teams.try_into().unwrap(),
  40. None => serde_json::json!({}),
  41. };
  42. let mut teams_config = serde_json::json!({});
  43. for (team_name, members) in teams.as_object().unwrap() {
  44. let members: Vec<_> = members.as_array().unwrap().iter().map(|member| {
  45. serde_json::json!({"name": member, "github": member, "github_id": 1, "is_lead": false})
  46. }).collect();
  47. teams_config[team_name] = serde_json::json!({
  48. "name": team_name,
  49. "kind": "team",
  50. "members": serde_json::Value::Array(members),
  51. "alumni": [],
  52. "discord": [],
  53. });
  54. }
  55. let teams = serde_json::value::from_value(teams_config).unwrap();
  56. let config = config.try_into().unwrap();
  57. let issue = serde_json::value::from_value(issue).unwrap();
  58. (teams, config, issue)
  59. }
  60. fn generic_issue(author: &str, repo: &str) -> serde_json::Value {
  61. serde_json::json!({
  62. "number": 1234,
  63. "created_at": "2022-06-26T21:31:31Z",
  64. "updated_at": "2022-06-26T21:31:31Z",
  65. "title": "Example PR",
  66. "body": "PR body",
  67. "html_url": "https://github.com/rust-lang/rust/pull/1234",
  68. "user": {
  69. "login": author,
  70. "id": 583231,
  71. },
  72. "labels": [],
  73. "assignees": [],
  74. "comments_url": format!("https://api.github.com/repos/{repo}/pull/1234/comments"),
  75. "state": "open",
  76. })
  77. }
  78. #[test]
  79. fn circular_groups() {
  80. // A cycle in the groups map.
  81. let config = toml::toml!(
  82. [adhoc_groups]
  83. compiler = ["other"]
  84. other = ["compiler"]
  85. );
  86. let issue = generic_issue("octocat", "rust-lang/rust");
  87. test_from_names(
  88. None,
  89. config,
  90. issue,
  91. &["compiler"],
  92. Err(FindReviewerError::NoReviewer {
  93. initial: vec!["compiler".to_string()],
  94. }),
  95. );
  96. }
  97. #[test]
  98. fn nested_groups() {
  99. // Test choosing a reviewer from group with nested groups.
  100. let config = toml::toml!(
  101. [adhoc_groups]
  102. a = ["@pnkfelix"]
  103. b = ["@nrc"]
  104. c = ["a", "b"]
  105. );
  106. let issue = generic_issue("octocat", "rust-lang/rust");
  107. test_from_names(None, config, issue, &["c"], Ok(&["nrc", "pnkfelix"]));
  108. }
  109. #[test]
  110. fn candidate_filtered_author_only_candidate() {
  111. // When the author is the only candidate.
  112. let config = toml::toml!(
  113. [adhoc_groups]
  114. compiler = ["nikomatsakis"]
  115. );
  116. let issue = generic_issue("nikomatsakis", "rust-lang/rust");
  117. test_from_names(
  118. None,
  119. config,
  120. issue,
  121. &["compiler"],
  122. Err(FindReviewerError::AllReviewersFiltered {
  123. initial: vec!["compiler".to_string()],
  124. filtered: vec!["nikomatsakis".to_string()],
  125. }),
  126. );
  127. }
  128. #[test]
  129. fn candidate_filtered_author() {
  130. // Filter out the author from the candidates.
  131. let config = toml::toml!(
  132. [adhoc_groups]
  133. compiler = ["user1", "user2", "user3", "group2"]
  134. group2 = ["user2", "user4"]
  135. );
  136. let issue = generic_issue("user2", "rust-lang/rust");
  137. test_from_names(
  138. None,
  139. config,
  140. issue,
  141. &["compiler"],
  142. Ok(&["user1", "user3", "user4"]),
  143. );
  144. }
  145. #[test]
  146. fn candidate_filtered_assignee() {
  147. // Filter out an existing assignee from the candidates.
  148. let config = toml::toml!(
  149. [adhoc_groups]
  150. compiler = ["user1", "user2", "user3", "user4"]
  151. );
  152. let mut issue = generic_issue("user2", "rust-lang/rust");
  153. issue["assignees"] = serde_json::json!([
  154. {"login": "user1", "id": 1},
  155. {"login": "user3", "id": 3},
  156. ]);
  157. test_from_names(None, config, issue, &["compiler"], Ok(&["user4"]));
  158. }
  159. #[test]
  160. fn groups_teams_users() {
  161. // Assortment of groups, teams, and users all selected at once.
  162. let teams = toml::toml!(
  163. team1 = ["t-user1"]
  164. team2 = ["t-user2"]
  165. );
  166. let config = toml::toml!(
  167. [adhoc_groups]
  168. group1 = ["user1", "rust-lang/team2"]
  169. );
  170. let issue = generic_issue("octocat", "rust-lang/rust");
  171. test_from_names(
  172. Some(teams),
  173. config,
  174. issue,
  175. &["team1", "group1", "user3"],
  176. Ok(&["t-user1", "t-user2", "user1", "user3"]),
  177. );
  178. }
  179. #[test]
  180. fn group_team_user_precedence() {
  181. // How it handles ambiguity when names overlap.
  182. let teams = toml::toml!(compiler = ["t-user1"]);
  183. let config = toml::toml!(
  184. [adhoc_groups]
  185. compiler = ["user2"]
  186. );
  187. let issue = generic_issue("octocat", "rust-lang/rust");
  188. test_from_names(
  189. Some(teams.clone()),
  190. config.clone(),
  191. issue.clone(),
  192. &["compiler"],
  193. Ok(&["user2"]),
  194. );
  195. test_from_names(
  196. Some(teams.clone()),
  197. config.clone(),
  198. issue.clone(),
  199. &["rust-lang/compiler"],
  200. Ok(&["user2"]),
  201. );
  202. }
  203. #[test]
  204. fn what_do_slashes_mean() {
  205. // How slashed names are handled.
  206. let teams = toml::toml!(compiler = ["t-user1"]);
  207. let config = toml::toml!(
  208. [adhoc_groups]
  209. compiler = ["user2"]
  210. "foo/bar" = ["foo-user"]
  211. );
  212. let issue = generic_issue("octocat", "rust-lang-nursery/rust");
  213. // Random slash names should work from groups.
  214. test_from_names(
  215. Some(teams.clone()),
  216. config.clone(),
  217. issue.clone(),
  218. &["foo/bar"],
  219. Ok(&["foo-user"]),
  220. );
  221. // Since this is rust-lang-nursery, it uses the rust-lang team, not the group.
  222. test_from_names(
  223. Some(teams.clone()),
  224. config.clone(),
  225. issue.clone(),
  226. &["rust-lang/compiler"],
  227. Ok(&["t-user1"]),
  228. );
  229. test_from_names(
  230. Some(teams.clone()),
  231. config.clone(),
  232. issue.clone(),
  233. &["rust-lang-nursery/compiler"],
  234. Ok(&["user2"]),
  235. );
  236. }
  237. #[test]
  238. fn invalid_org_doesnt_match() {
  239. let teams = toml::toml!(compiler = ["t-user1"]);
  240. let config = toml::toml!(
  241. [adhoc_groups]
  242. compiler = ["user2"]
  243. );
  244. let issue = generic_issue("octocat", "rust-lang/rust");
  245. test_from_names(
  246. Some(teams),
  247. config,
  248. issue,
  249. &["github/compiler"],
  250. Err(FindReviewerError::TeamNotFound(
  251. "github/compiler".to_string(),
  252. )),
  253. );
  254. }
  255. #[test]
  256. fn vacation() {
  257. let teams = toml::toml!(bootstrap = ["jyn514", "Mark-Simulacrum"]);
  258. let config = toml::toml!(users_on_vacation = ["jyn514"]);
  259. let issue = generic_issue("octocat", "rust-lang/rust");
  260. // Test that `r? user` falls through to assigning from the team.
  261. // See `determine_assignee` - ideally we would test that function directly instead of indirectly through `find_reviewer_from_names`.
  262. let err_names = vec!["jyn514".into()];
  263. test_from_names(
  264. Some(teams.clone()),
  265. config.clone(),
  266. issue.clone(),
  267. &["jyn514"],
  268. Err(FindReviewerError::AllReviewersFiltered {
  269. initial: err_names.clone(),
  270. filtered: err_names,
  271. }),
  272. );
  273. // Test that `r? bootstrap` doesn't assign from users on vacation.
  274. test_from_names(
  275. Some(teams.clone()),
  276. config.clone(),
  277. issue,
  278. &["bootstrap"],
  279. Ok(&["Mark-Simulacrum"]),
  280. );
  281. }