tests_candidates.rs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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. "roles": [],
  54. });
  55. }
  56. let teams = serde_json::value::from_value(teams_config).unwrap();
  57. let config = config.try_into().unwrap();
  58. let issue = serde_json::value::from_value(issue).unwrap();
  59. (teams, config, issue)
  60. }
  61. fn generic_issue(author: &str, repo: &str) -> serde_json::Value {
  62. serde_json::json!({
  63. "number": 1234,
  64. "created_at": "2022-06-26T21:31:31Z",
  65. "updated_at": "2022-06-26T21:31:31Z",
  66. "title": "Example PR",
  67. "body": "PR body",
  68. "html_url": "https://github.com/rust-lang/rust/pull/1234",
  69. "user": {
  70. "login": author,
  71. "id": 583231,
  72. },
  73. "labels": [],
  74. "assignees": [],
  75. "comments_url": format!("https://api.github.com/repos/{repo}/pull/1234/comments"),
  76. "state": "open",
  77. })
  78. }
  79. #[test]
  80. fn circular_groups() {
  81. // A cycle in the groups map.
  82. let config = toml::toml!(
  83. [adhoc_groups]
  84. compiler = ["other"]
  85. other = ["compiler"]
  86. );
  87. let issue = generic_issue("octocat", "rust-lang/rust");
  88. test_from_names(
  89. None,
  90. config,
  91. issue,
  92. &["compiler"],
  93. Err(FindReviewerError::NoReviewer {
  94. initial: vec!["compiler".to_string()],
  95. }),
  96. );
  97. }
  98. #[test]
  99. fn nested_groups() {
  100. // Test choosing a reviewer from group with nested groups.
  101. let config = toml::toml!(
  102. [adhoc_groups]
  103. a = ["@pnkfelix"]
  104. b = ["@nrc"]
  105. c = ["a", "b"]
  106. );
  107. let issue = generic_issue("octocat", "rust-lang/rust");
  108. test_from_names(None, config, issue, &["c"], Ok(&["nrc", "pnkfelix"]));
  109. }
  110. #[test]
  111. fn candidate_filtered_author_only_candidate() {
  112. // When the author is the only candidate.
  113. let config = toml::toml!(
  114. [adhoc_groups]
  115. compiler = ["nikomatsakis"]
  116. );
  117. let issue = generic_issue("nikomatsakis", "rust-lang/rust");
  118. test_from_names(
  119. None,
  120. config,
  121. issue,
  122. &["compiler"],
  123. Err(FindReviewerError::AllReviewersFiltered {
  124. initial: vec!["compiler".to_string()],
  125. filtered: vec!["nikomatsakis".to_string()],
  126. }),
  127. );
  128. }
  129. #[test]
  130. fn candidate_filtered_author() {
  131. // Filter out the author from the candidates.
  132. let config = toml::toml!(
  133. [adhoc_groups]
  134. compiler = ["user1", "user2", "user3", "group2"]
  135. group2 = ["user2", "user4"]
  136. );
  137. let issue = generic_issue("user2", "rust-lang/rust");
  138. test_from_names(
  139. None,
  140. config,
  141. issue,
  142. &["compiler"],
  143. Ok(&["user1", "user3", "user4"]),
  144. );
  145. }
  146. #[test]
  147. fn candidate_filtered_assignee() {
  148. // Filter out an existing assignee from the candidates.
  149. let config = toml::toml!(
  150. [adhoc_groups]
  151. compiler = ["user1", "user2", "user3", "user4"]
  152. );
  153. let mut issue = generic_issue("user2", "rust-lang/rust");
  154. issue["assignees"] = serde_json::json!([
  155. {"login": "user1", "id": 1},
  156. {"login": "user3", "id": 3},
  157. ]);
  158. test_from_names(None, config, issue, &["compiler"], Ok(&["user4"]));
  159. }
  160. #[test]
  161. fn groups_teams_users() {
  162. // Assortment of groups, teams, and users all selected at once.
  163. let teams = toml::toml!(
  164. team1 = ["t-user1"]
  165. team2 = ["t-user2"]
  166. );
  167. let config = toml::toml!(
  168. [adhoc_groups]
  169. group1 = ["user1", "rust-lang/team2"]
  170. );
  171. let issue = generic_issue("octocat", "rust-lang/rust");
  172. test_from_names(
  173. Some(teams),
  174. config,
  175. issue,
  176. &["team1", "group1", "user3"],
  177. Ok(&["t-user1", "t-user2", "user1", "user3"]),
  178. );
  179. }
  180. #[test]
  181. fn group_team_user_precedence() {
  182. // How it handles ambiguity when names overlap.
  183. let teams = toml::toml!(compiler = ["t-user1"]);
  184. let config = toml::toml!(
  185. [adhoc_groups]
  186. compiler = ["user2"]
  187. );
  188. let issue = generic_issue("octocat", "rust-lang/rust");
  189. test_from_names(
  190. Some(teams.clone()),
  191. config.clone(),
  192. issue.clone(),
  193. &["compiler"],
  194. Ok(&["user2"]),
  195. );
  196. test_from_names(
  197. Some(teams.clone()),
  198. config.clone(),
  199. issue.clone(),
  200. &["rust-lang/compiler"],
  201. Ok(&["user2"]),
  202. );
  203. }
  204. #[test]
  205. fn what_do_slashes_mean() {
  206. // How slashed names are handled.
  207. let teams = toml::toml!(compiler = ["t-user1"]);
  208. let config = toml::toml!(
  209. [adhoc_groups]
  210. compiler = ["user2"]
  211. "foo/bar" = ["foo-user"]
  212. );
  213. let issue = generic_issue("octocat", "rust-lang-nursery/rust");
  214. // Random slash names should work from groups.
  215. test_from_names(
  216. Some(teams.clone()),
  217. config.clone(),
  218. issue.clone(),
  219. &["foo/bar"],
  220. Ok(&["foo-user"]),
  221. );
  222. // Since this is rust-lang-nursery, it uses the rust-lang team, not the group.
  223. test_from_names(
  224. Some(teams.clone()),
  225. config.clone(),
  226. issue.clone(),
  227. &["rust-lang/compiler"],
  228. Ok(&["t-user1"]),
  229. );
  230. test_from_names(
  231. Some(teams.clone()),
  232. config.clone(),
  233. issue.clone(),
  234. &["rust-lang-nursery/compiler"],
  235. Ok(&["user2"]),
  236. );
  237. }
  238. #[test]
  239. fn invalid_org_doesnt_match() {
  240. let teams = toml::toml!(compiler = ["t-user1"]);
  241. let config = toml::toml!(
  242. [adhoc_groups]
  243. compiler = ["user2"]
  244. );
  245. let issue = generic_issue("octocat", "rust-lang/rust");
  246. test_from_names(
  247. Some(teams),
  248. config,
  249. issue,
  250. &["github/compiler"],
  251. Err(FindReviewerError::TeamNotFound(
  252. "github/compiler".to_string(),
  253. )),
  254. );
  255. }
  256. #[test]
  257. fn vacation() {
  258. let teams = toml::toml!(bootstrap = ["jyn514", "Mark-Simulacrum"]);
  259. let config = toml::toml!(users_on_vacation = ["jyn514"]);
  260. let issue = generic_issue("octocat", "rust-lang/rust");
  261. // Test that `r? user` falls through to assigning from the team.
  262. // See `determine_assignee` - ideally we would test that function directly instead of indirectly through `find_reviewer_from_names`.
  263. let err_names = vec!["jyn514".into()];
  264. test_from_names(
  265. Some(teams.clone()),
  266. config.clone(),
  267. issue.clone(),
  268. &["jyn514"],
  269. Err(FindReviewerError::AllReviewersFiltered {
  270. initial: err_names.clone(),
  271. filtered: err_names,
  272. }),
  273. );
  274. // Test that `r? bootstrap` doesn't assign from users on vacation.
  275. test_from_names(
  276. Some(teams.clone()),
  277. config.clone(),
  278. issue,
  279. &["bootstrap"],
  280. Ok(&["Mark-Simulacrum"]),
  281. );
  282. }