• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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