Skip to content

Commit c8511c3

Browse files
authored
Tests to verify data extracted is accurate (#19)
1 parent 4ef5a03 commit c8511c3

File tree

25 files changed

+449
-61
lines changed

25 files changed

+449
-61
lines changed

.github/workflows/pr-check.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ jobs:
7777
- name: Check Conda version
7878
run: conda info --all
7979

80+
- name: Create Conda Environments
81+
run: |
82+
conda create -n test-env1 python=3.12 -y
83+
conda create -n test-env-no-python -y
84+
conda create -p ./prefix-envs/.conda1 python=3.12 -y
85+
conda create -p ./prefix-envs/.conda-nopy -y
86+
8087
- name: Install pipenv
8188
run: pip install pipenv
8289

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pet-homebrew/src/lib.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use environments::get_python_info;
77
use pet_core::{
88
os_environment::Environment, python_environment::PythonEnvironment, Locator, LocatorResult,
99
};
10-
use pet_utils::{env::PythonEnv, executable::resolve_symlink};
10+
use pet_utils::{
11+
env::PythonEnv,
12+
executable::{find_executables, resolve_symlink},
13+
};
1114
use std::{collections::HashSet, path::PathBuf};
1215

1316
mod env_variables;
@@ -84,24 +87,25 @@ impl Locator for Homebrew {
8487
let mut reported: HashSet<String> = HashSet::new();
8588
let mut environments: Vec<PythonEnvironment> = vec![];
8689
for homebrew_prefix_bin in get_homebrew_prefix_bin(&self.environment) {
87-
for file in std::fs::read_dir(&homebrew_prefix_bin)
88-
.ok()?
89-
.filter_map(Result::ok)
90-
.filter(|f| {
91-
let file_name = f.file_name().to_str().unwrap_or_default().to_lowercase();
92-
file_name.starts_with("python")
93-
// If this file name is `python3`, then ignore this for now.
90+
for file in find_executables(&homebrew_prefix_bin).iter().filter(|f| {
91+
let file_name = f
92+
.file_name()
93+
.unwrap_or_default()
94+
.to_str()
95+
.unwrap_or_default()
96+
.to_lowercase();
97+
file_name.starts_with("python")
98+
// If this file name is `python3`, then ignore this for now.
9499
// We would prefer to use `python3.x` instead of `python3`.
95100
// That way its more consistent and future proof
96101
&& file_name != "python3"
97102
&& file_name != "python"
98-
})
99-
{
103+
}) {
100104
// Sometimes we end up with other python installs in the Homebrew bin directory.
101105
// E.g. /usr/local/bin is treated as a location where homebrew can be found (homebrew bin)
102106
// However this is a very generic location, and we might end up with other python installs here.
103107
// Hence call `resolve` to correctly identify homebrew python installs.
104-
let env_to_resolve = PythonEnv::new(file.path(), None, None);
108+
let env_to_resolve = PythonEnv::new(file.clone(), None, None);
105109
if let Some(env) = resolve(&env_to_resolve, &mut reported) {
106110
environments.push(env);
107111
}

crates/pet-pyenv/src/environments.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use pet_core::{
99
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentCategory},
1010
LocatorResult,
1111
};
12-
use pet_utils::{executable::find_executable, pyvenv_cfg::PyVenvCfg};
12+
use pet_utils::{executable::find_executable, headers::Headers, pyvenv_cfg::PyVenvCfg};
1313
use regex::Regex;
1414
use std::{fs, path::Path, sync::Arc};
1515

@@ -72,6 +72,8 @@ pub fn get_pure_python_environment(
7272
) -> Option<PythonEnvironment> {
7373
let file_name = path.file_name()?.to_string_lossy().to_string();
7474
let version = get_version(&file_name)?;
75+
// If we can get the version from the header files, thats more accurate.
76+
let version = Headers::get_version(path).unwrap_or(version.clone());
7577

7678
let arch = if file_name.ends_with("-win32") {
7779
Some(Architecture::X86)

crates/pet-reporter/src/environment.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
use log::error;
45
use pet_core::{
56
arch::Architecture,
67
python_environment::{PythonEnvironment, PythonEnvironmentCategory},
@@ -116,3 +117,17 @@ impl Environment {
116117
}
117118
}
118119
}
120+
121+
pub fn get_environment_key(env: &PythonEnvironment) -> Option<&PathBuf> {
122+
if let Some(exe) = &env.executable {
123+
Some(exe)
124+
} else if let Some(prefix) = &env.prefix {
125+
Some(prefix)
126+
} else {
127+
error!(
128+
"Failed to report environment due to lack of exe & prefix: {:?}",
129+
env
130+
);
131+
None
132+
}
133+
}

crates/pet-reporter/src/jsonrpc.rs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::{environment::Environment, manager::Manager};
4+
use crate::{
5+
environment::{get_environment_key, Environment},
6+
manager::Manager,
7+
};
58
use env_logger::Builder;
6-
use log::{error, LevelFilter};
9+
use log::LevelFilter;
710
use pet_core::{manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter};
811
use pet_jsonrpc::send_message;
912
use serde::{Deserialize, Serialize};
@@ -45,20 +48,6 @@ pub fn create_reporter() -> impl Reporter {
4548
}
4649
}
4750

48-
fn get_environment_key(env: &PythonEnvironment) -> Option<&PathBuf> {
49-
if let Some(exe) = &env.executable {
50-
Some(exe)
51-
} else if let Some(prefix) = &env.prefix {
52-
Some(prefix)
53-
} else {
54-
error!(
55-
"Failed to report environment due to lack of exe & prefix: {:?}",
56-
env
57-
);
58-
None
59-
}
60-
}
61-
6251
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]
6352
pub enum LogLevel {
6453
#[serde(rename = "debug")]

crates/pet-reporter/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ mod environment;
55
pub mod jsonrpc;
66
mod manager;
77
pub mod stdio;
8+
pub mod test;

crates/pet-reporter/src/stdio.rs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::{environment::Environment, manager::Manager};
4+
use crate::{
5+
environment::{get_environment_key, Environment},
6+
manager::Manager,
7+
};
58
use env_logger::Builder;
6-
use log::{error, LevelFilter};
9+
use log::LevelFilter;
710
use pet_core::{manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter};
811
use serde::{Deserialize, Serialize};
912
use std::{
@@ -46,20 +49,6 @@ pub fn create_reporter() -> impl Reporter {
4649
}
4750
}
4851

49-
fn get_environment_key(env: &PythonEnvironment) -> Option<&PathBuf> {
50-
if let Some(exe) = &env.executable {
51-
Some(exe)
52-
} else if let Some(prefix) = &env.prefix {
53-
Some(prefix)
54-
} else {
55-
error!(
56-
"Failed to report environment due to lack of exe & prefix: {:?}",
57-
env
58-
);
59-
None
60-
}
61-
}
62-
6352
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)]
6453
pub enum LogLevel {
6554
#[serde(rename = "debug")]

crates/pet-reporter/src/test.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::environment::get_environment_key;
5+
use pet_core::{manager::EnvManager, python_environment::PythonEnvironment, reporter::Reporter};
6+
use std::collections::HashMap;
7+
use std::{
8+
path::PathBuf,
9+
sync::{Arc, Mutex},
10+
};
11+
12+
pub struct TestReporter {
13+
pub reported_managers: Arc<Mutex<HashMap<PathBuf, EnvManager>>>,
14+
pub reported_environments: Arc<Mutex<HashMap<PathBuf, PythonEnvironment>>>,
15+
}
16+
17+
impl Reporter for TestReporter {
18+
fn report_manager(&self, manager: &EnvManager) {
19+
let mut reported_managers = self.reported_managers.lock().unwrap();
20+
if !reported_managers.contains_key(&manager.executable) {
21+
reported_managers.insert(manager.executable.clone(), manager.clone());
22+
}
23+
}
24+
25+
fn report_environment(&self, env: &PythonEnvironment) {
26+
if let Some(key) = get_environment_key(env) {
27+
let mut reported_environments = self.reported_environments.lock().unwrap();
28+
if !reported_environments.contains_key(key) {
29+
reported_environments.insert(key.clone(), env.clone());
30+
}
31+
}
32+
}
33+
}
34+
35+
pub fn create_reporter() -> TestReporter {
36+
TestReporter {
37+
reported_managers: Arc::new(Mutex::new(HashMap::new())),
38+
reported_environments: Arc::new(Mutex::new(HashMap::new())),
39+
}
40+
}

crates/pet-utils/src/executable.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,22 +103,24 @@ fn is_python_executable_name(exe: &Path) -> bool {
103103
// If the real file == exe, then it is not a symlink.
104104
pub fn resolve_symlink(exe: &Path) -> Option<PathBuf> {
105105
let name = exe.file_name()?.to_string_lossy();
106-
// TODO: What is -config and -build?
106+
// In bin directory of homebrew, we have files like python-build, python-config, python3-config
107107
if !name.starts_with("python") || name.ends_with("-config") || name.ends_with("-build") {
108108
return None;
109109
}
110-
111-
// If the file == symlink, then it is not a symlink.
112-
// We already have the resolved file, no need to return that again.
113-
if let Ok(real_file) = fs::read_link(exe) {
114-
if real_file == exe {
115-
None
116-
} else {
117-
Some(real_file)
110+
if let Ok(metadata) = std::fs::symlink_metadata(exe) {
111+
if metadata.is_file() || !metadata.file_type().is_symlink() {
112+
return Some(exe.to_path_buf());
118113
}
119-
} else {
120-
None
114+
if let Ok(readlink) = std::fs::canonicalize(exe) {
115+
if readlink == exe {
116+
return None;
117+
} else {
118+
return Some(readlink);
119+
}
120+
}
121+
return Some(exe.to_path_buf());
121122
}
123+
Some(exe.to_path_buf())
122124
}
123125

124126
// Given a list of executables, return the one with the shortest path.

crates/pet-utils/src/headers.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use regex::Regex;
66
use std::{fs, path::Path};
77

88
lazy_static! {
9-
static ref VERSION: Regex = Regex::new(r#"#define\s+PY_VERSION\s+"((\d+\.?)*)"#)
9+
static ref VERSION: Regex = Regex::new(r#"#define\s+PY_VERSION\s+"((\d+\.?)*.*)\""#)
1010
.expect("error parsing Version regex for partchlevel.h");
1111
}
1212

@@ -33,9 +33,31 @@ pub fn get_version(path: &Path) -> Option<String> {
3333
if path.ends_with(bin) {
3434
path.pop();
3535
}
36-
let headers_path = if cfg!(windows) { "Headers" } else { "include" };
37-
let patchlevel_h = path.join(headers_path).join("patchlevel.h");
38-
let contents = fs::read_to_string(patchlevel_h).ok()?;
36+
let headers_path = path.join(if cfg!(windows) { "Headers" } else { "include" });
37+
let patchlevel_h = headers_path.join("patchlevel.h");
38+
let mut contents = "".to_string();
39+
if let Ok(result) = fs::read_to_string(patchlevel_h) {
40+
contents = result;
41+
} else if fs::metadata(&headers_path).is_err() {
42+
// Such a path does not exist, get out.
43+
return None;
44+
} else {
45+
// Try the other path
46+
// Sometimes we have it in a sub directory such as `python3.10`
47+
if let Ok(readdir) = fs::read_dir(&headers_path) {
48+
for path in readdir.filter_map(Result::ok).map(|e| e.path()) {
49+
if let Ok(metadata) = fs::metadata(&path) {
50+
if metadata.is_dir() {
51+
let patchlevel_h = path.join("patchlevel.h");
52+
if let Ok(result) = fs::read_to_string(patchlevel_h) {
53+
contents = result;
54+
break;
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
3961
for line in contents.lines() {
4062
if let Some(captures) = VERSION.captures(line) {
4163
if let Some(value) = captures.get(1) {

crates/pet-utils/src/path.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ use std::{
88

99
// Similar to fs::canonicalize, but ignores UNC paths and returns the path as is (for windows).
1010
pub fn normalize<P: AsRef<Path>>(path: P) -> PathBuf {
11+
// On unix do not use canonicalize, results in weird issues with homebrew paths
12+
if cfg!(unix) {
13+
return path.as_ref().to_path_buf();
14+
}
15+
1116
if let Ok(resolved) = fs::canonicalize(&path) {
1217
if cfg!(unix) {
1318
return resolved;

crates/pet-utils/tests/sys_prefix_test.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,12 @@ fn version_from_header_files() {
7474
let path: PathBuf = resolve_test_path(&["unix", "headers", "python3.9.9", "bin"]).into();
7575
let version = SysPrefix::get_version(&path).unwrap();
7676
assert_eq!(version, "3.9.9");
77+
78+
let path: PathBuf = resolve_test_path(&["unix", "headers", "python3.10-dev", "bin"]).into();
79+
let version = SysPrefix::get_version(&path).unwrap();
80+
assert_eq!(version, "3.10.14+");
81+
82+
let path: PathBuf = resolve_test_path(&["unix", "headers", "python3.13", "bin"]).into();
83+
let version = SysPrefix::get_version(&path).unwrap();
84+
assert_eq!(version, "3.13.0a5");
7785
}

crates/pet-utils/tests/unix/headers/python3.10-dev/bin/python3

Whitespace-only changes.

crates/pet-utils/tests/unix/headers/python3.10-dev/bin/python3.9.9

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
/* Python version identification scheme.
3+
4+
When the major or minor version changes, the VERSION variable in
5+
configure.ac must also be changed.
6+
7+
There is also (independent) API version information in modsupport.h.
8+
*/
9+
10+
/* Values for PY_RELEASE_LEVEL */
11+
#define PY_RELEASE_LEVEL_ALPHA 0xA
12+
#define PY_RELEASE_LEVEL_BETA 0xB
13+
#define PY_RELEASE_LEVEL_GAMMA 0xC /* For release candidates */
14+
#define PY_RELEASE_LEVEL_FINAL 0xF /* Serial should be 0 here */
15+
/* Higher for patch releases */
16+
17+
/* Version parsed out into numeric values */
18+
/*--start constants--*/
19+
#define PY_MAJOR_VERSION 3
20+
#define PY_MINOR_VERSION 10
21+
#define PY_MICRO_VERSION 14
22+
#define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL
23+
#define PY_RELEASE_SERIAL 0
24+
25+
/* Version as a string */
26+
#define PY_VERSION "3.10.14+"
27+
/*--end constants--*/
28+
29+
/* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2.
30+
Use this for numeric comparisons, e.g. #if PY_VERSION_HEX >= ... */
31+
#define PY_VERSION_HEX ((PY_MAJOR_VERSION << 24) | \
32+
(PY_MINOR_VERSION << 16) | \
33+
(PY_MICRO_VERSION << 8) | \
34+
(PY_RELEASE_LEVEL << 4) | \
35+
(PY_RELEASE_SERIAL << 0))

crates/pet-utils/tests/unix/headers/python3.13/bin/python3

Whitespace-only changes.

crates/pet-utils/tests/unix/headers/python3.13/bin/python3.9.9

Whitespace-only changes.

0 commit comments

Comments
 (0)