Current section

Files

Jump to
erlang_python test py_venv_SUITE.erl
Raw

test/py_venv_SUITE.erl

%% Copyright 2026 Benoit Chesneau
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(py_venv_SUITE).
-include_lib("common_test/include/ct.hrl").
-include_lib("kernel/include/file.hrl").
-export([
all/0,
groups/0,
init_per_suite/1,
end_per_suite/1,
init_per_group/2,
end_per_group/2,
init_per_testcase/2,
end_per_testcase/2
]).
-export([
test_ensure_venv_creates_venv/1,
test_ensure_venv_activates_existing/1,
test_ensure_venv_with_requirements/1,
test_ensure_venv_force_recreate/1,
test_activate_venv/1,
test_deactivate_venv/1,
test_venv_info/1
]).
all() ->
[{group, venv_tests}].
groups() ->
[{venv_tests, [sequence], [
test_ensure_venv_creates_venv,
test_ensure_venv_activates_existing,
test_ensure_venv_with_requirements,
test_ensure_venv_force_recreate,
test_activate_venv,
test_deactivate_venv,
test_venv_info
]}].
init_per_suite(Config) ->
application:ensure_all_started(erlang_python),
Config.
end_per_suite(_Config) ->
ok.
init_per_group(_Group, Config) ->
%% Get Python executable path from the running interpreter
%% Note: sys.executable returns beam.smp when embedded, so we find the actual Python
%% Use a single expression to avoid any exec issues
Expr = <<"(lambda: next((p for p in [__import__('os').path.join(__import__('sys').prefix, 'bin', f'python{__import__(\"sys\").version_info.major}.{__import__(\"sys\").version_info.minor}'), __import__('os').path.join(__import__('sys').prefix, 'bin', 'python3'), __import__('os').path.join(__import__('sys').prefix, 'bin', 'python')] if __import__('os').path.isfile(p)), 'python3'))()">>,
{ok, PythonPath} = py:eval(Expr),
[{python_path, binary_to_list(PythonPath)} | Config].
end_per_group(_Group, _Config) ->
ok.
%% @private Create venv using the Python from config
create_test_venv(VenvPath, Config) ->
PythonPath = ?config(python_path, Config),
Cmd = PythonPath ++ " -m venv " ++ VenvPath,
_ = os:cmd(Cmd),
ok.
init_per_testcase(_TestCase, Config) ->
%% Create unique temp directory for each test
TempDir = filename:join(["/tmp", "py_venv_test_" ++ integer_to_list(erlang:unique_integer([positive]))]),
filelib:ensure_dir(filename:join(TempDir, "dummy")),
[{temp_dir, TempDir} | Config].
end_per_testcase(_TestCase, Config) ->
%% Clean up temp directory
TempDir = ?config(temp_dir, Config),
os:cmd("rm -rf " ++ TempDir),
%% Deactivate any active venv
py:deactivate_venv(),
ok.
%%% ============================================================================
%%% Test Cases
%%% ============================================================================
test_ensure_venv_creates_venv(Config) ->
TempDir = ?config(temp_dir, Config),
VenvPath = filename:join(TempDir, "venv"),
ReqFile = filename:join(TempDir, "requirements.txt"),
%% Create empty requirements file
ok = file:write_file(ReqFile, <<"# empty\n">>),
%% ensure_venv should create the venv and activate it
ok = py:ensure_venv(VenvPath, ReqFile, [{installer, pip}]),
%% Verify venv was created
true = filelib:is_file(filename:join(VenvPath, "pyvenv.cfg")),
%% Verify venv is active
{ok, Info} = py:venv_info(),
true = maps:get(<<"active">>, Info),
ok.
test_ensure_venv_activates_existing(Config) ->
TempDir = ?config(temp_dir, Config),
VenvPath = filename:join(TempDir, "venv"),
ReqFile = filename:join(TempDir, "requirements.txt"),
%% Create empty requirements file
ok = file:write_file(ReqFile, <<"# empty\n">>),
%% Create venv first time
ok = py:ensure_venv(VenvPath, ReqFile, [{installer, pip}]),
%% Deactivate
ok = py:deactivate_venv(),
{ok, Info1} = py:venv_info(),
false = maps:get(<<"active">>, Info1),
%% ensure_venv again should just activate existing venv (not recreate)
ok = py:ensure_venv(VenvPath, ReqFile, [{installer, pip}]),
%% Verify venv is active again
{ok, Info2} = py:venv_info(),
true = maps:get(<<"active">>, Info2),
ok.
test_ensure_venv_with_requirements(Config) ->
TempDir = ?config(temp_dir, Config),
VenvPath = filename:join(TempDir, "venv"),
ReqFile = filename:join(TempDir, "requirements.txt"),
%% Create requirements file with a simple package
ok = file:write_file(ReqFile, <<"six\n">>),
%% ensure_venv should create venv and install six
ok = py:ensure_venv(VenvPath, ReqFile, [{installer, pip}]),
%% Verify six is importable
{ok, Version} = py:eval(<<"__import__('six').__version__">>),
true = is_binary(Version),
ok.
test_ensure_venv_force_recreate(Config) ->
TempDir = ?config(temp_dir, Config),
VenvPath = filename:join(TempDir, "venv"),
ReqFile = filename:join(TempDir, "requirements.txt"),
%% Create empty requirements
ok = file:write_file(ReqFile, <<"# empty\n">>),
%% Create venv first time
ok = py:ensure_venv(VenvPath, ReqFile, [{installer, pip}]),
%% Get the pyvenv.cfg mtime
{ok, Info1} = file:read_file_info(filename:join(VenvPath, "pyvenv.cfg")),
Mtime1 = Info1#file_info.mtime,
%% Wait a bit
timer:sleep(1100),
%% Force recreate
ok = py:deactivate_venv(),
ok = py:ensure_venv(VenvPath, ReqFile, [{installer, pip}, force]),
%% Verify mtime changed (venv was recreated)
{ok, Info2} = file:read_file_info(filename:join(VenvPath, "pyvenv.cfg")),
Mtime2 = Info2#file_info.mtime,
true = Mtime2 > Mtime1,
ok.
test_activate_venv(Config) ->
TempDir = ?config(temp_dir, Config),
VenvPath = filename:join(TempDir, "venv"),
%% Create venv manually using the same Python we're linked against
ok = create_test_venv(VenvPath, Config),
%% Activate it
ok = py:activate_venv(VenvPath),
%% Verify active
{ok, Info} = py:venv_info(),
true = maps:get(<<"active">>, Info),
VenvBin = list_to_binary(VenvPath),
VenvBin = maps:get(<<"venv_path">>, Info),
ok.
test_deactivate_venv(Config) ->
TempDir = ?config(temp_dir, Config),
VenvPath = filename:join(TempDir, "venv"),
%% Create and activate venv using the same Python we're linked against
ok = create_test_venv(VenvPath, Config),
ok = py:activate_venv(VenvPath),
%% Verify active
{ok, Info1} = py:venv_info(),
true = maps:get(<<"active">>, Info1),
%% Deactivate
ok = py:deactivate_venv(),
%% Verify not active
{ok, Info2} = py:venv_info(),
false = maps:get(<<"active">>, Info2),
ok.
test_venv_info(Config) ->
TempDir = ?config(temp_dir, Config),
VenvPath = filename:join(TempDir, "venv"),
%% Ensure no venv is active from previous tests
py:deactivate_venv(),
%% Before activation, should be inactive
{ok, Info1} = py:venv_info(),
false = maps:get(<<"active">>, Info1),
%% Create and activate using the same Python we're linked against
ok = create_test_venv(VenvPath, Config),
ok = py:activate_venv(VenvPath),
%% After activation, should have all info
{ok, Info2} = py:venv_info(),
true = maps:get(<<"active">>, Info2),
true = is_binary(maps:get(<<"venv_path">>, Info2)),
true = is_binary(maps:get(<<"site_packages">>, Info2)),
true = is_list(maps:get(<<"sys_path">>, Info2)),
ok.