|
1 | 1 | use fs_err as fs; |
2 | | -use pixi_build_backend_passthrough::{ObservableBackend, PassthroughBackend}; |
| 2 | +use pixi_build_backend_passthrough::{BackendEvent, ObservableBackend, PassthroughBackend}; |
3 | 3 | use pixi_build_frontend::BackendOverride; |
4 | 4 | use pixi_consts::consts; |
5 | 5 | use rattler_conda_types::{Platform, package::RunExportsJson}; |
@@ -812,3 +812,228 @@ test-source-pkg = {{ path = "./source-package" }} |
812 | 812 | "Second invocation should report lock-file is already up to date" |
813 | 813 | ); |
814 | 814 | } |
| 815 | + |
| 816 | +/// Test that verifies changing `[package.build.config]` invalidates the metadata cache |
| 817 | +/// and causes the build backend to be re-queried. |
| 818 | +/// |
| 819 | +/// This tests the fix for issue #5309 where changes to build configuration |
| 820 | +/// (like `noarch = true` to `noarch = false`) did not invalidate the metadata cache. |
| 821 | +/// |
| 822 | +/// The test uses ObservableBackend to verify that the backend is called again |
| 823 | +/// when the configuration changes. |
| 824 | +#[tokio::test] |
| 825 | +async fn test_build_config_change_invalidates_cache() { |
| 826 | + setup_tracing(); |
| 827 | + |
| 828 | + // Create an observable passthrough backend to track calls |
| 829 | + let passthrough = PassthroughBackend::instantiator(); |
| 830 | + let (instantiator, mut observer) = ObservableBackend::instantiator(passthrough); |
| 831 | + let backend_override = BackendOverride::from_memory(instantiator); |
| 832 | + |
| 833 | + let pixi = PixiControl::new() |
| 834 | + .unwrap() |
| 835 | + .with_backend_override(backend_override); |
| 836 | + |
| 837 | + // Create a source package directory |
| 838 | + let source_dir = pixi.workspace_path().join("my-package"); |
| 839 | + fs::create_dir_all(&source_dir).unwrap(); |
| 840 | + |
| 841 | + // Create the source package manifest WITHOUT any [package.build.config] section |
| 842 | + let source_pixi_toml_no_config = r#" |
| 843 | +[package] |
| 844 | +name = "my-package" |
| 845 | +version = "1.0.0" |
| 846 | +
|
| 847 | +[package.build] |
| 848 | +backend = { name = "in-memory", version = "0.1.0" } |
| 849 | +"#; |
| 850 | + fs::write(source_dir.join("pixi.toml"), source_pixi_toml_no_config).unwrap(); |
| 851 | + |
| 852 | + // Create the workspace manifest |
| 853 | + let manifest_content = format!( |
| 854 | + r#" |
| 855 | +[workspace] |
| 856 | +channels = [] |
| 857 | +platforms = ["{}"] |
| 858 | +preview = ["pixi-build"] |
| 859 | +
|
| 860 | +[dependencies] |
| 861 | +my-package = {{ path = "./my-package" }} |
| 862 | +"#, |
| 863 | + Platform::current() |
| 864 | + ); |
| 865 | + |
| 866 | + fs::write(pixi.manifest_path(), manifest_content).unwrap(); |
| 867 | + |
| 868 | + // Helper to filter CondaOutputsCalled events |
| 869 | + fn count_conda_outputs_events(events: &[BackendEvent]) -> usize { |
| 870 | + events |
| 871 | + .iter() |
| 872 | + .filter(|e| matches!(e, BackendEvent::CondaOutputsCalled)) |
| 873 | + .count() |
| 874 | + } |
| 875 | + |
| 876 | + // First invocation: Generate the lock-file (no config section) |
| 877 | + let workspace = pixi.workspace().unwrap(); |
| 878 | + let (lock_file_data, was_updated) = workspace |
| 879 | + .update_lock_file(pixi_core::UpdateLockFileOptions::default()) |
| 880 | + .await |
| 881 | + .expect("First lock file generation should succeed"); |
| 882 | + |
| 883 | + assert!(was_updated, "First invocation should create the lock-file"); |
| 884 | + |
| 885 | + // Verify the package is in the lock-file |
| 886 | + let lock_file = lock_file_data.into_lock_file(); |
| 887 | + assert!( |
| 888 | + lock_file.contains_conda_package( |
| 889 | + consts::DEFAULT_ENVIRONMENT_NAME, |
| 890 | + Platform::current(), |
| 891 | + "my-package", |
| 892 | + ), |
| 893 | + "Lock file should contain my-package" |
| 894 | + ); |
| 895 | + |
| 896 | + // Check that conda_outputs was called once |
| 897 | + let events_after_first = observer.events(); |
| 898 | + assert_eq!( |
| 899 | + count_conda_outputs_events(&events_after_first), |
| 900 | + 1, |
| 901 | + "conda_outputs should be called once for first lock file generation" |
| 902 | + ); |
| 903 | + |
| 904 | + // Now add an EMPTY [package.build.config] section |
| 905 | + // This should NOT invalidate the cache since empty config hashes the same as no config |
| 906 | + let source_pixi_toml_empty_config = r#" |
| 907 | +[package] |
| 908 | +name = "my-package" |
| 909 | +version = "1.0.0" |
| 910 | +
|
| 911 | +[package.build] |
| 912 | +backend = { name = "in-memory", version = "0.1.0" } |
| 913 | +
|
| 914 | +[package.build.config] |
| 915 | +"#; |
| 916 | + fs::write(source_dir.join("pixi.toml"), source_pixi_toml_empty_config).unwrap(); |
| 917 | + |
| 918 | + // Second invocation with empty config section: Should NOT call backend again (cache hit) |
| 919 | + let workspace = pixi.workspace().unwrap(); |
| 920 | + let (_lock_file_data, was_updated_empty_config) = workspace |
| 921 | + .update_lock_file(pixi_core::UpdateLockFileOptions::default()) |
| 922 | + .await |
| 923 | + .expect("Second lock file check should succeed"); |
| 924 | + |
| 925 | + assert!( |
| 926 | + !was_updated_empty_config, |
| 927 | + "Adding empty [package.build.config] should NOT update lock-file" |
| 928 | + ); |
| 929 | + |
| 930 | + // Verify no additional conda_outputs calls |
| 931 | + let events_after_empty_config = observer.events(); |
| 932 | + assert_eq!( |
| 933 | + count_conda_outputs_events(&events_after_empty_config), |
| 934 | + 0, |
| 935 | + "conda_outputs should NOT be called when adding empty [package.build.config] (cache hit)" |
| 936 | + ); |
| 937 | + |
| 938 | + // Now add actual configuration values |
| 939 | + let source_pixi_toml_with_config = r#" |
| 940 | +[package] |
| 941 | +name = "my-package" |
| 942 | +version = "1.0.0" |
| 943 | +
|
| 944 | +[package.build] |
| 945 | +backend = { name = "in-memory", version = "0.1.0" } |
| 946 | +
|
| 947 | +[package.build.config] |
| 948 | +noarch = true |
| 949 | +"#; |
| 950 | + fs::write(source_dir.join("pixi.toml"), source_pixi_toml_with_config).unwrap(); |
| 951 | + |
| 952 | + // Third invocation: Should detect config change and call backend again |
| 953 | + let workspace = pixi.workspace().unwrap(); |
| 954 | + let (_lock_file_data, _was_updated_after_config_added) = workspace |
| 955 | + .update_lock_file(pixi_core::UpdateLockFileOptions::default()) |
| 956 | + .await |
| 957 | + .expect("Third lock file generation should succeed"); |
| 958 | + |
| 959 | + // Verify conda_outputs was called again due to config change |
| 960 | + let events_after_config_added = observer.events(); |
| 961 | + assert_eq!( |
| 962 | + count_conda_outputs_events(&events_after_config_added), |
| 963 | + 1, |
| 964 | + "conda_outputs should be called when [package.build.config] gets actual values (cache invalidated)" |
| 965 | + ); |
| 966 | + |
| 967 | + // Fourth invocation without changes: Should NOT call backend again (cache hit) |
| 968 | + let workspace = pixi.workspace().unwrap(); |
| 969 | + let (_lock_file_data, was_updated_no_change) = workspace |
| 970 | + .update_lock_file(pixi_core::UpdateLockFileOptions::default()) |
| 971 | + .await |
| 972 | + .expect("Fourth lock file check should succeed"); |
| 973 | + |
| 974 | + assert!( |
| 975 | + !was_updated_no_change, |
| 976 | + "Fourth invocation without changes should NOT update lock-file" |
| 977 | + ); |
| 978 | + |
| 979 | + // Verify no additional conda_outputs calls |
| 980 | + let events_after_no_change = observer.events(); |
| 981 | + assert_eq!( |
| 982 | + count_conda_outputs_events(&events_after_no_change), |
| 983 | + 0, |
| 984 | + "conda_outputs should NOT be called again when config hasn't changed (cache hit)" |
| 985 | + ); |
| 986 | + |
| 987 | + // Now change the build configuration (noarch = true -> noarch = false) |
| 988 | + let source_pixi_toml_changed_config = r#" |
| 989 | +[package] |
| 990 | +name = "my-package" |
| 991 | +version = "1.0.0" |
| 992 | +
|
| 993 | +[package.build] |
| 994 | +backend = { name = "in-memory", version = "0.1.0" } |
| 995 | +
|
| 996 | +[package.build.config] |
| 997 | +noarch = false |
| 998 | +"#; |
| 999 | + fs::write( |
| 1000 | + source_dir.join("pixi.toml"), |
| 1001 | + source_pixi_toml_changed_config, |
| 1002 | + ) |
| 1003 | + .unwrap(); |
| 1004 | + |
| 1005 | + // Fifth invocation: Should detect config change and call backend again |
| 1006 | + let workspace = pixi.workspace().unwrap(); |
| 1007 | + let (_lock_file_data, _was_updated_after_config_change) = workspace |
| 1008 | + .update_lock_file(pixi_core::UpdateLockFileOptions::default()) |
| 1009 | + .await |
| 1010 | + .expect("Fifth lock file generation should succeed"); |
| 1011 | + |
| 1012 | + // Verify conda_outputs was called again due to config change |
| 1013 | + let events_after_config_change = observer.events(); |
| 1014 | + assert_eq!( |
| 1015 | + count_conda_outputs_events(&events_after_config_change), |
| 1016 | + 1, |
| 1017 | + "conda_outputs should be called again when [package.build.config] values change (cache invalidated)" |
| 1018 | + ); |
| 1019 | + |
| 1020 | + // Sixth invocation: Should NOT call backend again (cache is now fresh) |
| 1021 | + let workspace = pixi.workspace().unwrap(); |
| 1022 | + let (_, was_updated_sixth) = workspace |
| 1023 | + .update_lock_file(pixi_core::UpdateLockFileOptions::default()) |
| 1024 | + .await |
| 1025 | + .expect("Sixth lock file check should succeed"); |
| 1026 | + |
| 1027 | + assert!( |
| 1028 | + !was_updated_sixth, |
| 1029 | + "Sixth invocation should NOT update lock-file (cache is now fresh)" |
| 1030 | + ); |
| 1031 | + |
| 1032 | + // Verify no additional conda_outputs calls |
| 1033 | + let events_after_sixth = observer.events(); |
| 1034 | + assert_eq!( |
| 1035 | + count_conda_outputs_events(&events_after_sixth), |
| 1036 | + 0, |
| 1037 | + "conda_outputs should NOT be called again after cache is updated" |
| 1038 | + ); |
| 1039 | +} |
0 commit comments