1 /*
2 * Copyright (C) 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 //! Functions to extract finalized flag information from
17 //! /prebuilts/sdk/#/finalized-flags.txt.
18 //! These functions are very specific to that file setup as well as the format
19 //! of the files (just a list of the fully-qualified flag names).
20 //! There are also some helper functions for local building using cargo. These
21 //! functions are only invoked via cargo for quick local testing and will not
22 //! be used during actual soong building. They are marked as such.
23 use anyhow::{anyhow, Result};
24 use serde::{Deserialize, Serialize};
25 use std::collections::{HashMap, HashSet};
26 use std::fs;
27 use std::io::{self, BufRead};
28
29 const SDK_INT_MULTIPLIER: u32 = 100_000;
30
31 /// Just the fully qualified flag name (package_name.flag_name).
32 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
33 pub struct FinalizedFlag {
34 /// Name of the flag.
35 pub flag_name: String,
36 /// Name of the package.
37 pub package_name: String,
38 }
39
40 /// API level in which the flag was finalized.
41 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
42 pub struct ApiLevel(pub i32);
43
44 /// API level of the extended flags file of version 35
45 pub const EXTENDED_FLAGS_35_APILEVEL: ApiLevel = ApiLevel(35);
46
47 /// Contains all flags finalized for a given API level.
48 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
49 pub struct FinalizedFlagMap(HashMap<ApiLevel, HashSet<FinalizedFlag>>);
50
51 impl FinalizedFlagMap {
52 /// Creates a new, empty instance.
new() -> Self53 pub fn new() -> Self {
54 Self(HashMap::new())
55 }
56
57 /// Convenience method for is_empty on the underlying map.
is_empty(&self) -> bool58 pub fn is_empty(&self) -> bool {
59 self.0.is_empty()
60 }
61
62 /// Returns the API level in which the flag was finalized .
get_finalized_level(&self, flag: &FinalizedFlag) -> Option<ApiLevel>63 pub fn get_finalized_level(&self, flag: &FinalizedFlag) -> Option<ApiLevel> {
64 for (api_level, flags_for_level) in &self.0 {
65 if flags_for_level.contains(flag) {
66 return Some(*api_level);
67 }
68 }
69 None
70 }
71
72 /// Insert the flag into the map for the given level if the flag is not
73 /// present in the map already - for *any* level (not just the one given).
insert_if_new(&mut self, level: ApiLevel, flag: FinalizedFlag)74 pub fn insert_if_new(&mut self, level: ApiLevel, flag: FinalizedFlag) {
75 if self.contains(&flag) {
76 return;
77 }
78 self.0.entry(level).or_default().insert(flag);
79 }
80
contains(&self, flag: &FinalizedFlag) -> bool81 fn contains(&self, flag: &FinalizedFlag) -> bool {
82 self.0.values().any(|flags_set| flags_set.contains(flag))
83 }
84 }
85
86 #[allow(dead_code)] // TODO: b/378936061: Use with SDK_INT_FULL check.
parse_full_version(version: String) -> Result<u32>87 fn parse_full_version(version: String) -> Result<u32> {
88 let (major, minor) = if let Some(decimal_index) = version.find('.') {
89 (version[..decimal_index].parse::<u32>()?, version[decimal_index + 1..].parse::<u32>()?)
90 } else {
91 (version.parse::<u32>()?, 0)
92 };
93
94 if major >= 21474 {
95 return Err(anyhow!("Major version too large, must be less than 21474."));
96 }
97 if minor >= SDK_INT_MULTIPLIER {
98 return Err(anyhow!("Minor version too large, must be less than {}.", SDK_INT_MULTIPLIER));
99 }
100
101 Ok(major * SDK_INT_MULTIPLIER + minor)
102 }
103
104 const EXTENDED_FLAGS_LIST_35: &str = "extended_flags_list_35.txt";
105
106 /// Converts a string to an int. Will parse to int even if the string is "X.0".
107 /// Returns error for "X.1".
str_to_api_level(numeric_string: &str) -> Result<ApiLevel>108 fn str_to_api_level(numeric_string: &str) -> Result<ApiLevel> {
109 let float_value = numeric_string.parse::<f64>()?;
110
111 if float_value.fract() == 0.0 {
112 Ok(ApiLevel(float_value as i32))
113 } else {
114 Err(anyhow!("Numeric string is float, can't parse to int."))
115 }
116 }
117
118 /// For each file, extracts the qualified flag names into a FinalizedFlag, then
119 /// enters them in a map at the API level corresponding to their directory.
120 /// Ex: /prebuilts/sdk/35/finalized-flags.txt -> {36, [flag1, flag2]}.
read_files_to_map_using_path(flag_files: Vec<String>) -> Result<FinalizedFlagMap>121 pub fn read_files_to_map_using_path(flag_files: Vec<String>) -> Result<FinalizedFlagMap> {
122 let mut data_map = FinalizedFlagMap::new();
123
124 for flag_file in flag_files {
125 // Split /path/sdk/<int.int>/finalized-flags.txt -> ['/path/sdk', 'int.int', 'finalized-flags.txt'].
126 let flag_file_split: Vec<String> =
127 flag_file.clone().rsplitn(3, '/').map(|s| s.to_string()).collect();
128
129 if &flag_file_split[0] != "finalized-flags.txt" {
130 return Err(anyhow!("Provided incorrect file, must be finalized-flags.txt"));
131 }
132
133 let api_level_string = &flag_file_split[1];
134
135 // For now, skip any directory with full API level, e.g. "36.1". The
136 // finalized flag files each contain all flags finalized *up to* that
137 // level (including prior levels), so skipping intermediate levels means
138 // the flags will be included at the next full number.
139 // TODO: b/378936061 - Support full SDK version.
140 // In the future, we should error if provided a non-numeric directory.
141 let Ok(api_level) = str_to_api_level(api_level_string) else {
142 continue;
143 };
144
145 let file = fs::File::open(&flag_file)?;
146
147 io::BufReader::new(file).lines().for_each(|flag| {
148 let flag =
149 flag.unwrap_or_else(|_| panic!("Failed to read line from file {}", flag_file));
150 let finalized_flag = build_finalized_flag(&flag)
151 .unwrap_or_else(|_| panic!("cannot build finalized flag {}", flag));
152 data_map.insert_if_new(api_level, finalized_flag);
153 });
154 }
155
156 Ok(data_map)
157 }
158
159 /// Read the qualified flag names into a FinalizedFlag set
read_extend_file_to_map_using_path(extened_file: String) -> Result<HashSet<FinalizedFlag>>160 pub fn read_extend_file_to_map_using_path(extened_file: String) -> Result<HashSet<FinalizedFlag>> {
161 let (_, file_name) =
162 extened_file.rsplit_once('/').ok_or(anyhow!("Invalid file: '{}'", extened_file))?;
163 if file_name != EXTENDED_FLAGS_LIST_35 {
164 return Err(anyhow!("Provided incorrect file, must be {}", EXTENDED_FLAGS_LIST_35));
165 }
166 let file = fs::File::open(extened_file)?;
167 let extended_flags = io::BufReader::new(file)
168 .lines()
169 .map(|flag| {
170 let flag = flag.expect("Failed to read line from extended file");
171 build_finalized_flag(&flag)
172 .unwrap_or_else(|_| panic!("cannot build finalized flag {}", flag))
173 })
174 .collect::<HashSet<FinalizedFlag>>();
175 Ok(extended_flags)
176 }
177
build_finalized_flag(qualified_flag_name: &String) -> Result<FinalizedFlag>178 fn build_finalized_flag(qualified_flag_name: &String) -> Result<FinalizedFlag> {
179 // Split the qualified flag name into package and flag name:
180 // com.my.package.name.my_flag_name -> ('com.my.package.name', 'my_flag_name')
181 let (package_name, flag_name) = qualified_flag_name
182 .rsplit_once('.')
183 .ok_or(anyhow!("Invalid qualified flag name format: '{}'", qualified_flag_name))?;
184
185 Ok(FinalizedFlag { flag_name: flag_name.to_string(), package_name: package_name.to_string() })
186 }
187
188 #[cfg(test)]
189 mod tests {
190 use super::*;
191 use std::fs::File;
192 use std::io::Write;
193 use tempfile::tempdir;
194
195 const FLAG_FILE_NAME: &str = "finalized-flags.txt";
196
197 // Creates some flags for testing.
create_test_flags() -> Vec<FinalizedFlag>198 fn create_test_flags() -> Vec<FinalizedFlag> {
199 vec![
200 FinalizedFlag { flag_name: "name1".to_string(), package_name: "package1".to_string() },
201 FinalizedFlag { flag_name: "name2".to_string(), package_name: "package2".to_string() },
202 FinalizedFlag { flag_name: "name3".to_string(), package_name: "package3".to_string() },
203 ]
204 }
205
206 // Writes the fully qualified flag names in the given file.
add_flags_to_file(flag_file: &mut File, flags: &[FinalizedFlag])207 fn add_flags_to_file(flag_file: &mut File, flags: &[FinalizedFlag]) {
208 for flag in flags {
209 let _unused = writeln!(flag_file, "{}.{}", flag.package_name, flag.flag_name);
210 }
211 }
212
213 #[test]
test_read_flags_one_file()214 fn test_read_flags_one_file() {
215 let flags = create_test_flags();
216
217 // Create the file <temp_dir>/35/finalized-flags.txt.
218 let temp_dir = tempdir().unwrap();
219 let mut file_path = temp_dir.path().to_path_buf();
220 file_path.push("35");
221 fs::create_dir_all(&file_path).unwrap();
222 file_path.push(FLAG_FILE_NAME);
223 let mut file = File::create(&file_path).unwrap();
224
225 // Write all flags to the file.
226 add_flags_to_file(&mut file, &[flags[0].clone(), flags[1].clone()]);
227 let flag_file_path = file_path.to_string_lossy().to_string();
228
229 // Convert to map.
230 let map = read_files_to_map_using_path(vec![flag_file_path]).unwrap();
231
232 assert_eq!(map.0.len(), 1);
233 assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0]));
234 assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[1]));
235 }
236
237 #[test]
test_read_flags_two_files()238 fn test_read_flags_two_files() {
239 let flags = create_test_flags();
240
241 // Create the file <temp_dir>/35/finalized-flags.txt and for 36.
242 let temp_dir = tempdir().unwrap();
243 let mut file_path1 = temp_dir.path().to_path_buf();
244 file_path1.push("35");
245 fs::create_dir_all(&file_path1).unwrap();
246 file_path1.push(FLAG_FILE_NAME);
247 let mut file1 = File::create(&file_path1).unwrap();
248
249 let mut file_path2 = temp_dir.path().to_path_buf();
250 file_path2.push("36");
251 fs::create_dir_all(&file_path2).unwrap();
252 file_path2.push(FLAG_FILE_NAME);
253 let mut file2 = File::create(&file_path2).unwrap();
254
255 // Write all flags to the files.
256 add_flags_to_file(&mut file1, &[flags[0].clone()]);
257 add_flags_to_file(&mut file2, &[flags[0].clone(), flags[1].clone(), flags[2].clone()]);
258 let flag_file_path1 = file_path1.to_string_lossy().to_string();
259 let flag_file_path2 = file_path2.to_string_lossy().to_string();
260
261 // Convert to map.
262 let map = read_files_to_map_using_path(vec![flag_file_path1, flag_file_path2]).unwrap();
263
264 // Assert there are two API levels, 35 and 36.
265 assert_eq!(map.0.len(), 2);
266 assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0]));
267
268 // 36 should not have the first flag in the set, as it was finalized in
269 // an earlier API level.
270 assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[1]));
271 assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[2]));
272 }
273
274 #[test]
test_read_flags_full_numbers()275 fn test_read_flags_full_numbers() {
276 let flags = create_test_flags();
277
278 // Create the file <temp_dir>/35/finalized-flags.txt and for 36.
279 let temp_dir = tempdir().unwrap();
280 let mut file_path1 = temp_dir.path().to_path_buf();
281 file_path1.push("35.0");
282 fs::create_dir_all(&file_path1).unwrap();
283 file_path1.push(FLAG_FILE_NAME);
284 let mut file1 = File::create(&file_path1).unwrap();
285
286 let mut file_path2 = temp_dir.path().to_path_buf();
287 file_path2.push("36.0");
288 fs::create_dir_all(&file_path2).unwrap();
289 file_path2.push(FLAG_FILE_NAME);
290 let mut file2 = File::create(&file_path2).unwrap();
291
292 // Write all flags to the files.
293 add_flags_to_file(&mut file1, &[flags[0].clone()]);
294 add_flags_to_file(&mut file2, &[flags[0].clone(), flags[1].clone(), flags[2].clone()]);
295 let flag_file_path1 = file_path1.to_string_lossy().to_string();
296 let flag_file_path2 = file_path2.to_string_lossy().to_string();
297
298 // Convert to map.
299 let map = read_files_to_map_using_path(vec![flag_file_path1, flag_file_path2]).unwrap();
300
301 assert_eq!(map.0.len(), 2);
302 assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0]));
303 assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[1]));
304 assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[2]));
305 }
306
307 #[test]
test_read_flags_fractions_round_up()308 fn test_read_flags_fractions_round_up() {
309 let flags = create_test_flags();
310
311 // Create the file <temp_dir>/35/finalized-flags.txt and for 36.
312 let temp_dir = tempdir().unwrap();
313 let mut file_path1 = temp_dir.path().to_path_buf();
314 file_path1.push("35.1");
315 fs::create_dir_all(&file_path1).unwrap();
316 file_path1.push(FLAG_FILE_NAME);
317 let mut file1 = File::create(&file_path1).unwrap();
318
319 let mut file_path2 = temp_dir.path().to_path_buf();
320 file_path2.push("36.0");
321 fs::create_dir_all(&file_path2).unwrap();
322 file_path2.push(FLAG_FILE_NAME);
323 let mut file2 = File::create(&file_path2).unwrap();
324
325 // Write all flags to the files.
326 add_flags_to_file(&mut file1, &[flags[0].clone()]);
327 add_flags_to_file(&mut file2, &[flags[0].clone(), flags[1].clone(), flags[2].clone()]);
328 let flag_file_path1 = file_path1.to_string_lossy().to_string();
329 let flag_file_path2 = file_path2.to_string_lossy().to_string();
330
331 // Convert to map.
332 let map = read_files_to_map_using_path(vec![flag_file_path1, flag_file_path2]).unwrap();
333
334 // No flags were added in 35. All 35.1 flags were rolled up to 36.
335 assert_eq!(map.0.len(), 1);
336 assert!(!map.0.contains_key(&ApiLevel(35)));
337 assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[0]));
338 assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[1]));
339 assert!(map.0.get(&ApiLevel(36)).unwrap().contains(&flags[2]));
340 }
341
342 #[test]
test_read_flags_non_numeric()343 fn test_read_flags_non_numeric() {
344 let flags = create_test_flags();
345
346 // Create the file <temp_dir>/35/finalized-flags.txt.
347 let temp_dir = tempdir().unwrap();
348 let mut file_path = temp_dir.path().to_path_buf();
349 file_path.push("35");
350 fs::create_dir_all(&file_path).unwrap();
351 file_path.push(FLAG_FILE_NAME);
352 let mut flag_file = File::create(&file_path).unwrap();
353
354 let mut invalid_path = temp_dir.path().to_path_buf();
355 invalid_path.push("sdk-annotations");
356 fs::create_dir_all(&invalid_path).unwrap();
357 invalid_path.push(FLAG_FILE_NAME);
358 File::create(&invalid_path).unwrap();
359
360 // Write all flags to the file.
361 add_flags_to_file(&mut flag_file, &[flags[0].clone(), flags[1].clone()]);
362 let flag_file_path = file_path.to_string_lossy().to_string();
363
364 // Convert to map.
365 let map = read_files_to_map_using_path(vec![
366 flag_file_path,
367 invalid_path.to_string_lossy().to_string(),
368 ])
369 .unwrap();
370
371 // No set should be created for sdk-annotations.
372 assert_eq!(map.0.len(), 1);
373 assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[0]));
374 assert!(map.0.get(&ApiLevel(35)).unwrap().contains(&flags[1]));
375 }
376
377 #[test]
test_read_flags_wrong_file_err()378 fn test_read_flags_wrong_file_err() {
379 let flags = create_test_flags();
380
381 // Create the file <temp_dir>/35/finalized-flags.txt.
382 let temp_dir = tempdir().unwrap();
383 let mut file_path = temp_dir.path().to_path_buf();
384 file_path.push("35");
385 fs::create_dir_all(&file_path).unwrap();
386 file_path.push(FLAG_FILE_NAME);
387 let mut flag_file = File::create(&file_path).unwrap();
388
389 let mut pre_flag_path = temp_dir.path().to_path_buf();
390 pre_flag_path.push("18");
391 fs::create_dir_all(&pre_flag_path).unwrap();
392 pre_flag_path.push("some_random_file.txt");
393 File::create(&pre_flag_path).unwrap();
394
395 // Write all flags to the file.
396 add_flags_to_file(&mut flag_file, &[flags[0].clone(), flags[1].clone()]);
397 let flag_file_path = file_path.to_string_lossy().to_string();
398
399 // Convert to map.
400 let map = read_files_to_map_using_path(vec![
401 flag_file_path,
402 pre_flag_path.to_string_lossy().to_string(),
403 ]);
404
405 assert!(map.is_err());
406 }
407
408 #[test]
test_flags_map_insert_if_new()409 fn test_flags_map_insert_if_new() {
410 let flags = create_test_flags();
411 let mut map = FinalizedFlagMap::new();
412 let l35 = ApiLevel(35);
413 let l36 = ApiLevel(36);
414
415 map.insert_if_new(l35, flags[0].clone());
416 map.insert_if_new(l35, flags[1].clone());
417 map.insert_if_new(l35, flags[2].clone());
418 map.insert_if_new(l36, flags[0].clone());
419
420 assert!(map.0.get(&l35).unwrap().contains(&flags[0]));
421 assert!(map.0.get(&l35).unwrap().contains(&flags[1]));
422 assert!(map.0.get(&l35).unwrap().contains(&flags[2]));
423 assert!(!map.0.contains_key(&l36));
424 }
425
426 #[test]
test_flags_map_get_level()427 fn test_flags_map_get_level() {
428 let flags = create_test_flags();
429 let mut map = FinalizedFlagMap::new();
430 let l35 = ApiLevel(35);
431 let l36 = ApiLevel(36);
432
433 map.insert_if_new(l35, flags[0].clone());
434 map.insert_if_new(l36, flags[1].clone());
435
436 assert_eq!(map.get_finalized_level(&flags[0]).unwrap(), l35);
437 assert_eq!(map.get_finalized_level(&flags[1]).unwrap(), l36);
438 }
439
440 #[test]
test_read_flag_from_extended_file()441 fn test_read_flag_from_extended_file() {
442 let flags = create_test_flags();
443
444 // Create the file <temp_dir>/35/extended_flags_list_35.txt
445 let temp_dir = tempdir().unwrap();
446 let mut file_path = temp_dir.path().to_path_buf();
447 file_path.push("35");
448 fs::create_dir_all(&file_path).unwrap();
449 file_path.push(EXTENDED_FLAGS_LIST_35);
450 let mut file = File::create(&file_path).unwrap();
451
452 // Write all flags to the file.
453 add_flags_to_file(&mut file, &[flags[0].clone(), flags[1].clone()]);
454
455 let flags_set =
456 read_extend_file_to_map_using_path(file_path.to_string_lossy().to_string()).unwrap();
457 assert_eq!(flags_set.len(), 2);
458 assert!(flags_set.contains(&flags[0]));
459 assert!(flags_set.contains(&flags[1]));
460 }
461
462 #[test]
test_read_flag_from_wrong_extended_file_err()463 fn test_read_flag_from_wrong_extended_file_err() {
464 let flags = create_test_flags();
465
466 // Create the file <temp_dir>/35/extended_flags_list.txt
467 let temp_dir = tempdir().unwrap();
468 let mut file_path = temp_dir.path().to_path_buf();
469 file_path.push("35");
470 fs::create_dir_all(&file_path).unwrap();
471 file_path.push("extended_flags_list.txt");
472 let mut file = File::create(&file_path).unwrap();
473
474 // Write all flags to the file.
475 add_flags_to_file(&mut file, &[flags[0].clone(), flags[1].clone()]);
476
477 let err = read_extend_file_to_map_using_path(file_path.to_string_lossy().to_string())
478 .unwrap_err();
479 assert_eq!(
480 format!("{:?}", err),
481 "Provided incorrect file, must be extended_flags_list_35.txt"
482 );
483 }
484
485 #[test]
test_parse_full_version_correct_input_major_dot_minor()486 fn test_parse_full_version_correct_input_major_dot_minor() {
487 let version = parse_full_version("12.34".to_string());
488
489 assert!(version.is_ok());
490 assert_eq!(version.unwrap(), 1_200_034);
491 }
492
493 #[test]
test_parse_full_version_correct_input_omit_dot_minor()494 fn test_parse_full_version_correct_input_omit_dot_minor() {
495 let version = parse_full_version("1234".to_string());
496
497 assert!(version.is_ok());
498 assert_eq!(version.unwrap(), 123_400_000);
499 }
500
501 #[test]
test_parse_full_version_incorrect_input_empty_string()502 fn test_parse_full_version_incorrect_input_empty_string() {
503 let version = parse_full_version("".to_string());
504
505 assert!(version.is_err());
506 }
507
508 #[test]
test_parse_full_version_incorrect_input_no_numbers_in_string()509 fn test_parse_full_version_incorrect_input_no_numbers_in_string() {
510 let version = parse_full_version("hello".to_string());
511
512 assert!(version.is_err());
513 }
514
515 #[test]
test_parse_full_version_incorrect_input_unexpected_patch_version()516 fn test_parse_full_version_incorrect_input_unexpected_patch_version() {
517 let version = parse_full_version("1.2.3".to_string());
518
519 assert!(version.is_err());
520 }
521
522 #[test]
test_parse_full_version_incorrect_input_leading_dot_missing_major_version()523 fn test_parse_full_version_incorrect_input_leading_dot_missing_major_version() {
524 let version = parse_full_version(".1234".to_string());
525
526 assert!(version.is_err());
527 }
528
529 #[test]
test_parse_full_version_incorrect_input_trailing_dot_missing_minor_version()530 fn test_parse_full_version_incorrect_input_trailing_dot_missing_minor_version() {
531 let version = parse_full_version("1234.".to_string());
532
533 assert!(version.is_err());
534 }
535
536 #[test]
test_parse_full_version_incorrect_input_negative_major_version()537 fn test_parse_full_version_incorrect_input_negative_major_version() {
538 let version = parse_full_version("-12.34".to_string());
539
540 assert!(version.is_err());
541 }
542
543 #[test]
test_parse_full_version_incorrect_input_negative_minor_version()544 fn test_parse_full_version_incorrect_input_negative_minor_version() {
545 let version = parse_full_version("12.-34".to_string());
546
547 assert!(version.is_err());
548 }
549
550 #[test]
test_parse_full_version_incorrect_input_major_version_too_large()551 fn test_parse_full_version_incorrect_input_major_version_too_large() {
552 let version = parse_full_version("40000.1".to_string());
553
554 assert!(version.is_err());
555 }
556
557 #[test]
test_parse_full_version_incorrect_input_minor_version_too_large()558 fn test_parse_full_version_incorrect_input_minor_version_too_large() {
559 let version = parse_full_version("3.99999999".to_string());
560
561 assert!(version.is_err());
562 }
563 }
564