Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ jobs:
AZURE_SIGNTOOL_KEY_VAULT_URL: ${{ secrets.AZURE_SIGNTOOL_KEY_VAULT_URL }}
CONSTRUCTOR_EXAMPLES_KEEP_ARTIFACTS: "${{ runner.temp }}/examples_artifacts"
CONSTRUCTOR_SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x86/signtool.exe"
CONSTRUCTOR_VERBOSE: 1
CONSTRUCTOR_VERBOSE: 0
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This I'll change back to 1 later.

run: |
rm -rf coverage.json
pytest -vv --cov=constructor --cov-branch tests/test_examples.py
Expand Down
7 changes: 7 additions & 0 deletions constructor/briefcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import functools
import logging
import os
import re
import shutil
import sys
Expand Down Expand Up @@ -249,6 +250,10 @@ class Payload:
archive_name: str = "payload.tar.gz"
conda_exe_name: str = "_conda.exe"

# There might be other ways we want to enable `add_debug_logging`, but it has proven
# very useful at least for the CI environment.
add_debug_logging: bool = bool(os.environ.get("CI")) and os.environ.get("CI") != "0"

@functools.cached_property
def root(self) -> Path:
"""Create root upon first access and cache it."""
Expand Down Expand Up @@ -332,6 +337,8 @@ def render_templates(self) -> list[str: TemplateFile]:
context: dict[str, str] = {
"archive_name": self.archive_name,
"conda_exe_name": self.conda_exe_name,
"add_debug": self.add_debug_logging,
"register_envs": str(self.info.get("register_envs", True)).lower(),
Comment thread
marcoesters marked this conversation as resolved.
}

# Render the templates now using jinja and the defined context
Expand Down
39 changes: 38 additions & 1 deletion constructor/briefcase/pre_uninstall.bat
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
set "INSTDIR=%cd%"
@echo {{ 'on' if add_debug else 'off' }}
setlocal

rem Assign INSTDIR and normalize the path
set "INSTDIR=%~dp0.."
for %%I in ("%INSTDIR%") do set "INSTDIR=%%~fI"

set "BASE_PATH=%INSTDIR%\base"
set "PREFIX=%BASE_PATH%"
set "CONDA_EXE=%INSTDIR%\{{ conda_exe_name }}"
set "PAYLOAD_TAR=%INSTDIR%\{{ archive_name }}"

{%- if add_debug %}
rem Get the name of the install directory
for %%I in ("%INSTDIR%") do set "APPNAME=%%~nxI"
set "LOG=%TEMP%\%APPNAME%-preuninstall.log"
Copy link
Copy Markdown
Owner Author

@lrandersson lrandersson Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really happy with this solution of setting the file name here, since it is also explicitly set in test_examples.py, ideally it would be only defined in one place instead of two as it is right now (same also applies for postinstall.log). But it does suffice for now and helped tremendously while debugging last week, I also don't see any need to change this line in the near future since it just works.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to access the log file for the MSI installer somehow?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean accessing it from the CI pipeline? Otherwise no, only by opening the file, I think it's a bit clunky, MSI has "support" for logging but it seems very generic since as you might have seen that we enable logging for MSI Installers via cmd.extend(["/L*V", str(log_path)]) - this doesn't include the logging for pre/post install/uninstall.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's why I'm asking if there is any avenue that we can access log_path from within these scripts.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it could work but it's risky. We can end-up with encoding mismatch, MSI log is usually UTF-16. cmd.exe echo writes in the console codepage (effectively “ANSI”/OEM), so you’ll end up mixing encodings in one file.
We can also end-up with weird/corrupted log files from the MSI engine trying to write at the same time as the scripts.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A problem to solve for a different PR, but I would really like to see those logged in the same file. Maybe we need to extend the logger to detect the file encoding of an existing log file and change it accordingly? The documentation is a little sparse about what happens when encoding is None.

Is the risk for race-condition really there? Would an MSI installer even write to log while the script is running?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer this correctly, I think we need to do a lot of testing. I agree something for a different PR.


echo ==== pre_uninstall start ==== >> "%LOG%"
echo SCRIPT=%~f0 >> "%LOG%"
echo CWD=%CD% >> "%LOG%"
echo INSTDIR=%INSTDIR% >> "%LOG%"
echo BASE_PATH=%BASE_PATH% >> "%LOG%"
echo CONDA_EXE=%CONDA_EXE% >> "%LOG%"
echo PAYLOAD_TAR=%PAYLOAD_TAR% >> "%LOG%"
"%CONDA_EXE%" --version >> "%LOG%" 2>&1
{%- endif %}

{%- set redir = ' >> "%LOG%" 2>&1' if add_debug else '' %}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conda-standalone has logging functionality you could use - it essentially works like the Tee-Object commandlet.

{%- set dump_and_exit = 'type "%LOG%" & exit /b %errorlevel%' if add_debug else 'exit /b %errorlevel%' %}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should always create a log if possible. The log level should just determine what goes into the log. We do have to make sure it gets deleted in the end though.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcoesters thanks for your first set of review comments. I thought I'd respond here first because the current design is due to some limitations I ran into last week, and I think we need to discuss this before I make any additional changes.

  1. I agree that we should always log to a file, unfortunately it doesn't seem possible to do this without redirecting all visible output from the user (the person who runs the installer) - when logging to a file, all you see is an empty console that closes after a while. I looked into this last week and the only way to use tee (or similar) is through PowerShell, which makes us "stuck" with above. Therefore I'm a bit reluctant to log all output to a file by default for the average user. Any thoughts on this?
  2. You mentioned; "conda-standalone has logging functionality you could use - it essentially works like the Tee-Object commandlet.". Will this also log exceptions that could occur when conda.exe is running? If yes this sounds like a good idea, the current approach will also make any stack traces from conda.exe visible when the tests are running in our CI environment. This is also how I discovered that we the canary builds use an older version of conda-standalone, because they caused stack traces that were not visible without redirecting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how the logger is set up: https://github.com/conda/conda-standalone/blob/00deca6accc6fd0b9dae8346f125f1ad1d107524/src/entry_point.py#L134-L164

So, it should catch all stdout and stderr, but you could try by running conda.exe python -c 'raise RuntimeError("Error message")' --log-file <path to log file> and see what happens.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried it and unfortunately it doesn't add unhandled exceptions to the log file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we found, that was an incorrect example, but it does work.


rem Sanity checks
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call these "consistency checks" instead of "sanity checks"?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still see some instances

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not exist "%CONDA_EXE%" (
{% if add_debug %}echo [ERROR] CONDA_EXE not found: "%CONDA_EXE%" >> "%LOG%" & type "%LOG%" & {% endif %}exit /b 10
)
Comment on lines +40 to +42
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we error out, we should always add a message. So, I'd skip the entire check if not debugging or always add the message.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we just always add to the log file? That answer may depend on how we deal with the MSI vs post-install logging inconsistency.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cant add everything that is outputted from the scripts to the log file while also displaying the output "live" to the person who runs the installer, see my answer in #3 (comment) (the first bullet).

Copy link
Copy Markdown
Owner Author

@lrandersson lrandersson Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we might be able to achieve "always write to file" (but not the same file for the MSI engine + pre/post uninstall/install), with the following approach:

  1. <all calls to conda-standalone utilize --log-file> // because it seems like its writing to stdout and well as the file? I've only tried so far with conda run python -c 'raise Exception(...)'
  2. All bat-file "echo ..." needs to be written twice, once without redirection and one with redirection

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workaround I used for NSIS was to write to a step-log file and then type to screen and append to the main log. I think logging will need its own PR though - it's a bigger challenge than anticipated.


rem Recreate an empty payload tar. This file was deleted during installation but the
rem MSI installer expects it to exist.
type nul > "%PAYLOAD_TAR%"
if errorlevel 1 (
{% if add_debug %}echo [ERROR] Failed to create "%PAYLOAD_TAR%" >> "%LOG%" & type "%LOG%" & {% endif %}exit /b %errorlevel%
)

"%CONDA_EXE%" constructor uninstall --prefix "%BASE_PATH%"{{ redir }}
if errorlevel 1 ( {{ dump_and_exit }} )

exit /b 0
58 changes: 50 additions & 8 deletions constructor/briefcase/run_installation.bat
Original file line number Diff line number Diff line change
@@ -1,20 +1,62 @@
set "INSTDIR=%cd%"
@echo {{ 'on' if add_debug else 'off' }}
setlocal

rem Assign INSTDIR and normalize the path
set "INSTDIR=%~dp0.."
for %%I in ("%INSTDIR%") do set "INSTDIR=%%~fI"

set "BASE_PATH=%INSTDIR%\base"
set "PREFIX=%BASE_PATH%"
set "CONDA_EXE=%INSTDIR%\{{ conda_exe_name }}"
set "PAYLOAD_TAR=%INSTDIR%\{{ archive_name }}"

echo "Unpacking payload..."
"%CONDA_EXE%" constructor extract --prefix "%INSTDIR%" --tar-from-stdin < "%PAYLOAD_TAR%"
"%CONDA_EXE%" constructor --prefix "%BASE_PATH%" --extract-conda-pkgs

set CONDA_EXTRA_SAFETY_CHECKS=no
set CONDA_PROTECT_FROZEN_ENVS=0
set "CONDA_ROOT_PREFIX=%BASE_PATH%"
set CONDA_REGISTER_ENVS={{ register_envs }}
set CONDA_SAFETY_CHECKS=disabled
set CONDA_EXTRA_SAFETY_CHECKS=no
set "CONDA_ROOT_PREFIX=%BASE_PATH%"
set "CONDA_PKGS_DIRS=%BASE_PATH%\pkgs"

"%CONDA_EXE%" install --offline --file "%BASE_PATH%\conda-meta\initial-state.explicit.txt" -yp "%BASE_PATH%"
{%- if add_debug %}
rem Get the name of the install directory
for %%I in ("%INSTDIR%") do set "APPNAME=%%~nxI"
set "LOG=%TEMP%\%APPNAME%-postinstall.log"

echo ==== run_installation start ==== >> "%LOG%"
echo SCRIPT=%~f0 >> "%LOG%"
echo CWD=%CD% >> "%LOG%"
echo INSTDIR=%INSTDIR% >> "%LOG%"
echo BASE_PATH=%BASE_PATH% >> "%LOG%"
echo CONDA_EXE=%CONDA_EXE% >> "%LOG%"
echo PAYLOAD_TAR=%PAYLOAD_TAR% >> "%LOG%"
{%- endif %}

{%- set redir = ' >> "%LOG%" 2>&1' if add_debug else '' %}
{%- set dump_and_exit = 'type "%LOG%" & exit /b %errorlevel%' if add_debug else 'exit /b %errorlevel%' %}

rem Sanity checks
if not exist "%CONDA_EXE%" (
{% if add_debug %}echo [ERROR] CONDA_EXE not found: "%CONDA_EXE%" >> "%LOG%" & type "%LOG%" & {% endif %}exit /b 10
Comment thread
marcoesters marked this conversation as resolved.
Outdated
)
if not exist "%PAYLOAD_TAR%" (
{% if add_debug %}echo [ERROR] PAYLOAD_TAR not found: "%PAYLOAD_TAR%" >> "%LOG%" & type "%LOG%" & {% endif %}exit /b 11
)

echo Unpacking payload...
rem "%CONDA_EXE%" constructor extract --prefix "%INSTDIR%" --tar-from-stdin < "%PAYLOAD_TAR%"{{ redir }}
"%CONDA_EXE%" constructor --prefix "%INSTDIR%" --extract-tarball < "%PAYLOAD_TAR%"{{ redir }}
if errorlevel 1 ( {{ dump_and_exit }} )

rem "%CONDA_EXE%" constructor --prefix "%BASE_PATH%" --extract-conda-pkgs{{ redir }}
"%CONDA_EXE%" constructor --prefix "%BASE_PATH%" --extract-conda-pkgs{{ redir }}
if errorlevel 1 ( {{ dump_and_exit }} )

if not exist "%BASE_PATH%" (
{% if add_debug %}echo [ERROR] "%BASE_PATH%" not found! >> "%LOG%" & type "%LOG%" & {% endif %}exit /b 12
)

"%CONDA_EXE%" install --offline --file "%BASE_PATH%\conda-meta\initial-state.explicit.txt" -yp "%BASE_PATH%"{{ redir }}
if errorlevel 1 ( {{ dump_and_exit }} )

rem Delete the payload to save disk space.
rem A truncated placeholder of 0 bytes is recreated during uninstall
Expand Down
2 changes: 1 addition & 1 deletion examples/register_envs/construct.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

name: RegisterEnvs
version: 1.0.0
installer_type: {{ "exe" if os.name == "nt" else "all" }}
installer_type: all
channels:
- https://repo.anaconda.com/pkgs/main/
specs:
Expand Down
169 changes: 139 additions & 30 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import ctypes
import getpass
import json
import os
Expand All @@ -10,6 +9,7 @@
import time
import warnings
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import timedelta
from functools import cache
from pathlib import Path
Expand Down Expand Up @@ -338,28 +338,99 @@ def _sentinel_file_checks(example_path, install_dir):
)


def is_admin() -> bool:
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except Exception:
return False


def calculate_msi_install_path(installer: Path) -> Path:
"""This is a temporary solution for now since we cannot choose the install location ourselves.
Installers are named <name>-<version>-Windows-x86_64.msi.
"""
dir_name = installer.name.replace("-Windows-x86_64.msi", "").replace("-", " ")
if is_admin():
Comment thread
marcoesters marked this conversation as resolved.
root_dir = Path(os.environ.get("PROGRAMFILES", r"C:\Program Files"))
else:
local_dir = os.environ.get("LOCALAPPDATA", str(Path.home() / r"AppData\Local"))
root_dir = Path(local_dir) / "Programs"
local_dir = os.environ.get("LOCALAPPDATA", str(Path.home() / r"AppData\Local"))
root_dir = Path(local_dir) / "Programs"
root_dir.mkdir(parents=True, exist_ok=True)

assert root_dir.is_dir() # Sanity check to avoid strange unexpected errors
return Path(root_dir) / dir_name


def handle_exception_and_error_out(
failure: InstallationFailure | UninstallationFailure, original_exception: BaseException
) -> None:
"""Print failure context (including logs) and re-raise with exception chaining."""
print(failure.read_text())
raise failure from original_exception


def _read_briefcase_log_tail(path: Path, last_digits: int) -> str:
"""Helper function to read logs from installers created with briefcase.
The encoding can vary between the different logs.
"""
if not path or not path.exists():
return f"(log not found: {path})"

# Try UTF-16 first (MSI logs), fallback to UTF-8
try:
text = path.read_text(encoding="utf-16", errors="replace")
except UnicodeError:
text = path.read_text(encoding="utf-8", errors="replace")

return text[last_digits:]


@dataclass
class InstallationFailure(RuntimeError):
Comment thread
marcoesters marked this conversation as resolved.
cmd: list[str]
returncode: int
msi_log: Path | None = None
post_install_log: Path | None = None

def read_text(self, last_digits: int = -15000) -> str:
parts = [
f"Command: {self.cmd}",
f"Return code: {self.returncode}",
]

if self.post_install_log:
parts.append(
f"\n=== MSI LOG POST INSTALL: {self.post_install_log} ===\n"
+ _read_briefcase_log_tail(self.post_install_log, last_digits)
)

if self.msi_log:
parts.append(
f"\n=== MSI LOG: {self.msi_log} ===\n"
+ _read_briefcase_log_tail(self.msi_log, last_digits)
)

return "\n".join(parts)


@dataclass
class UninstallationFailure(RuntimeError):
cmd: list[str]
returncode: int
msi_log: Path | None = None
pre_uninstall_log: Path | None = None

def read_text(self, last_digits: int = -15000) -> str:
parts = [
f"Command: {self.cmd}",
f"Return code: {self.returncode}",
]

if self.pre_uninstall_log:
parts.append(
f"\n=== MSI LOG PRE UNINSTALL: {self.pre_uninstall_log} ===\n"
+ _read_briefcase_log_tail(self.pre_uninstall_log, last_digits)
)

if self.msi_log:
parts.append(
f"\n=== MSI LOG: {self.msi_log} ===\n"
+ _read_briefcase_log_tail(self.msi_log, last_digits)
)

return "\n".join(parts)


def _run_installer_msi(
installer: Path,
install_dir: Path,
Expand Down Expand Up @@ -388,22 +459,41 @@ def _run_installer_msi(
"/qn",
]

log_path = Path(os.environ.get("TEMP")) / (install_dir.name + ".log")
# Prepare logging
post_install_log = Path(os.environ.get("TEMP")) / (install_dir.name + "-postinstall.log")
log_path = Path(os.environ.get("TEMP")) / (install_dir.name + "-install.log")
for log_file in [post_install_log, log_path]:
if log_file.exists():
os.remove(log_file)
cmd.extend(["/L*V", str(log_path)])

# Run installer and handle errors/logs if necessary
try:
process = _execute(cmd, installer_input=installer_input, timeout=timeout, check=check)
except subprocess.CalledProcessError as e:
if log_path.exists():
# When running on the CI system, it tries to decode a UTF-16 log file as UTF-8,
# therefore we need to specify encoding before printing.
print(f"\n=== MSI LOG {log_path} START ===")
print(
log_path.read_text(encoding="utf-16", errors="replace")[-15000:]
) # last 15k chars
print(f"\n=== MSI LOG {log_path} END ===")
raise e
if check:
print("A check for MSI Installers not yet implemented")
handle_exception_and_error_out(
InstallationFailure(
cmd=cmd,
returncode=e.returncode,
msi_log=log_path,
post_install_log=post_install_log,
),
original_exception=e,
)

# Sanity check the installation directory
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really needed but something I had use of while debugging last week. Let me know if I should remove it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be removed to keep the code cleaner.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to respond to this one, removed f39a787

expected_items = [
install_dir / "base",
install_dir / "base" / "conda-meta",
install_dir / "_conda.exe",
]
missing_items = [item for item in expected_items if not item.exists()]
if missing_items:
missing_items_string = "\n".join(missing_items)
raise Exception(
f"Sanity check failed, unable to find expected paths: \n{missing_items_string}"
)

return process


Expand All @@ -419,13 +509,32 @@ def _run_uninstaller_msi(
str(installer),
"/qn",
]
process = _execute(cmd, timeout=timeout, check=check)

# Prepare logging
pre_uninstall_log = Path(os.environ.get("TEMP")) / (install_dir.name + "-preuninstall.log")
log_path = Path(os.environ.get("TEMP")) / (install_dir.name + "-uninstall.log")
for log_file in [pre_uninstall_log, log_path]:
if log_file.exists():
os.remove(log_file)
cmd.extend(["/L*V", str(log_path)])

try:
process = _execute(cmd, installer_input=None, timeout=timeout, check=check)
except subprocess.CalledProcessError as e:
handle_exception_and_error_out(
UninstallationFailure(
cmd=cmd,
returncode=e.returncode,
msi_log=log_path,
post_install_log=pre_uninstall_log,
),
original_exception=e,
)

if check:
# TODO:
# Check log and if there are remaining files, similar to the exe installers
pass
# This is temporary until uninstallation works fine
shutil.rmtree(str(install_dir), ignore_errors=True)

return process

Expand Down Expand Up @@ -1051,8 +1160,6 @@ def test_register_envs(tmp_path, request):
"""Verify that 'register_envs: False' results in the environment not being registered."""
input_path = _example_path("register_envs")
for installer, install_dir in create_installer(input_path, tmp_path):
if installer.suffix == ".msi":
raise NotImplementedError("Test for 'register_envs' not yet implemented for MSI")
_run_installer(input_path, installer, install_dir, request=request)
environments_txt = Path("~/.conda/environments.txt").expanduser().read_text()
assert str(install_dir) not in environments_txt
Expand Down Expand Up @@ -1622,6 +1729,8 @@ def test_not_in_installed_menu_list_(tmp_path, request, no_registry):
input_path = _example_path("register_envs") # The specific example we use here is not important
options = ["/InstallationType=JustMe", f"/NoRegistry={no_registry}"]
for installer, install_dir in create_installer(input_path, tmp_path):
if installer.suffix == ".msi":
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is for the .exe flag /NoRegistry so it's not applicable for the .msi installers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just change the installer type to exe in the construct.yaml file then? Using pytest.skip will skip the whole test. At least, you should use continue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use continue because it will fail on the assert statements that follow after.
I wonder if we should move this test into a separate file, some background: The test is running the example register_envs, which was picked arbitrarily when creating the test because we just needed any example in order to test the /NoRegistry flag. Therefore changing the installer type is not feasible because we still need the installer type set to all for test_example_register_envs. We could also create a new example for exe only, but that doesn't seem to be the correct setup since the test is only intended for /NoRegistry.

This could also be solved in a more neat way using proper parametrization (related conda#1146)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertions won't fail because they are not inside the for-loop. The test seems to be written with the assumption that only exe installers are created for Windows. Using continue will create an MSI installer, but nothing will be done with it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's correct. I don't see however how using continue and moving the assert statements into the for-loop makes the test-skip more explicit over the current approach. It is unfortunately already clunky as is since the installer needs to be created in order to identify what kind of installer type we are testing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is that currently, your tests are marked as skipped: https://github.com/lrandersson/constructor/actions/runs/22183340055/job/64150179614?pr=3#step:12:82

The loop will first create the exe installer (due to the sorting) and then get to the msi installer. So, the tests aren't even executed for the exe installer since they are outside the for-loop.

I agree that it is clunky. We don't even need to move the test into the for-loop because in the end as long as we ensure that the exe installer is the only installer that has been run. I think this is the only thing we need to add:

if installer.suffix != "exe":
    continue

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that's tedious. but it makes sense now when you say that.
I updated it here 60f85bb, I did indent the assert anyway because I tried the continue approach earlier last week and it will fail on the assert statement for the second parametrized test (because it will find the program in the 'Installed Apps Menu').

pytest.skip("Test is only applicable for NSIS based installers.")
_run_installer(
input_path,
installer,
Expand Down
Loading