Skip to content

Commit 4396eab

Browse files
authored
[CI] Run tests on Windows (#101)
* More exhaustive method to locate vcvarsall.bat; mark some tests as non-Windows * Do not produce empty array literals, as MSVC does not allow them * Do not use gcc on Windows * Re-order array definitions before quantize() function * Skip test_sparse_categorical_model() test on Windows, see #100 * Free predictor objects between multiple tests, to avoid locking DLL files * Run unit tests on Windows * Replace *.whl extension with *.zip * Fix syntax error in PowerShell script * Fix lint * Use correct version of MSVC * Add conda to PATH * Install pytest and other packages * Expand short paths on Windows * Create temp directory locally * Fix lint * Debugging hung tests in Windows * Remove LETOR from testing on Windows * Remove verbose printing
1 parent 77b8442 commit 4396eab

File tree

11 files changed

+219
-107
lines changed

11 files changed

+219
-107
lines changed

azure-pipelines.yml

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
solution: 'build/*.sln'
8787
msbuildArchitecture: 'x64'
8888
msbuildArguments: '/p:Configuration=Release /m /nodeReuse:false'
89-
displayName: 'Building XGBoost...'
89+
displayName: 'Building Treelite...'
9090
- script: |
9191
call $(Agent.BuildDirectory)\CONDA\Scripts\activate
9292
cd python
@@ -225,3 +225,47 @@ jobs:
225225
displayName: 'Submitting code coverage data to CodeCov...'
226226
env:
227227
CODECOV_TOKEN: afe9868c-2c27-4853-89fa-4bc5d3d2b255
228+
229+
- job: win_python_test
230+
dependsOn: win_build
231+
pool:
232+
vmImage: 'vs2017-win2016'
233+
steps:
234+
- checkout: self
235+
submodules: recursive
236+
- powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts"
237+
displayName: 'Add conda to PATH'
238+
- script: |
239+
call activate
240+
conda install --yes --quiet numpy scipy scikit-learn pandas
241+
displayName: 'Setting up Python environment...'
242+
- task: DownloadPipelineArtifact@0
243+
inputs:
244+
artifactName: 'python_win_whl'
245+
targetPath: $(System.DefaultWorkingDirectory)
246+
displayName: 'Downloading Treelite Python wheel for Windows...'
247+
- powershell: |
248+
Dir *.whl | Rename-Item -newname { $_.name -replace ".whl", ".zip" }
249+
Expand-Archive *.zip -DestinationPath .\whl_content
250+
New-Item .\lib -ItemType Directory -ea 0
251+
New-Item .\runtime\native\lib -ItemType Directory -ea 0
252+
New-Item .\build -ItemType Directory -ea 0
253+
Move-Item -Path .\whl_content\treelite-*.data\data\treelite\treelite.dll -Destination .\lib
254+
Move-Item -Path .\whl_content\treelite-*.data\data\treelite\treelite_runtime.dll -Destination .\runtime\native\lib
255+
Remove-Item .\whl_content -Force -Recurse
256+
Set-Location -Path .\build
257+
cmake .. -G"Visual Studio 15 2017 Win64"
258+
displayName: 'Installing Treelite into Python environment...'
259+
- script: |
260+
call activate
261+
python -m pip install wheel setuptools xgboost lightgbm pytest pytest-cov
262+
python -m pytest -v --fulltrace tests\python --cov=./
263+
displayName: 'Running Python tests...'
264+
env:
265+
PYTHONPATH: .\python
266+
- script: |
267+
choco install codecov
268+
codecov
269+
displayName: 'Submitting code coverage data to CodeCov...'
270+
env:
271+
CODECOV_TOKEN: afe9868c-2c27-4853-89fa-4bc5d3d2b255

python/treelite/common/util.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
import ctypes
77
import inspect
88
import time
9-
import shutil
109
import os
1110
import sys
1211
import site
13-
from .compat import py_str, PY3
12+
from .compat import py_str
1413

1514
class TreeliteVersionNotFound(Exception):
1615
"""Error thrown by when version file is not found"""
@@ -50,22 +49,6 @@ def _load_ver():
5049
class TreeliteError(Exception):
5150
"""Error thrown by treelite"""
5251

53-
if PY3:
54-
# pylint: disable=W0611
55-
from tempfile import TemporaryDirectory
56-
else:
57-
import tempfile
58-
class TemporaryDirectory():
59-
"""Context manager for tempfile.mkdtemp()"""
60-
# pylint: disable=R0903
61-
62-
def __enter__(self):
63-
self.name = tempfile.mkdtemp() # pylint: disable=W0201
64-
return self.name
65-
66-
def __exit__(self, exc_type, exc_value, traceback):
67-
shutil.rmtree(self.name)
68-
6952
def lineno():
7053
"""Returns line number"""
7154
return inspect.currentframe().f_back.f_lineno

python/treelite/contrib/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,39 @@
44
Contrib API provides ways to interact with third-party libraries and tools.
55
"""
66

7+
import sys
78
import os
89
import json
910
import time
1011
import shutil
12+
import ctypes
1113
from ..common.util import TreeliteError, lineno, log_info
1214
from ..libpath import find_lib_path
1315
from .util import _libext, _toolchain_exist_check
1416

17+
def expand_windows_path(dirpath):
18+
"""
19+
Expand a short path to full path (only applicable for Windows)
20+
21+
Parameters
22+
----------
23+
dirpath : :py:class:`str <python:str>`
24+
Path to expand
25+
26+
Returns
27+
-------
28+
fullpath : :py:class:`str <python:str>`
29+
Expanded path
30+
"""
31+
if sys.platform == 'win32':
32+
BUFFER_SIZE = 500
33+
buffer = ctypes.create_unicode_buffer(BUFFER_SIZE)
34+
get_long_path_name = ctypes.windll.kernel32.GetLongPathNameW
35+
get_long_path_name.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint]
36+
get_long_path_name(dirpath, buffer, BUFFER_SIZE)
37+
return buffer.value
38+
return dirpath
39+
1540
def generate_makefile(dirpath, platform, toolchain, options=None):
1641
"""
1742
Generate a Makefile for a given directory of headers and sources. The
@@ -158,6 +183,7 @@ def create_shared(toolchain, dirpath, nthread=None, verbose=False, options=None)
158183

159184
if nthread is not None and nthread <= 0:
160185
raise TreeliteError('nthread must be positive integer')
186+
dirpath = expand_windows_path(dirpath)
161187
if not os.path.isdir(dirpath):
162188
raise TreeliteError('Directory {} does not exist'.format(dirpath))
163189
try:

python/treelite/contrib/msvc.py

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
from __future__ import absolute_import as _abs
77
import os
8-
from ..common.compat import PY3
8+
import glob
9+
import re
10+
from distutils.version import StrictVersion
911
from .util import _create_shared_base, _libext
1012

1113
LIBEXT = _libext()
@@ -14,25 +16,6 @@ def _is_64bit_windows():
1416
return 'PROGRAMFILES(X86)' in os.environ
1517

1618
def _varsall_bat_path():
17-
if PY3:
18-
import winreg # pylint: disable=E0401
19-
else:
20-
import _winreg as winreg # pylint: disable=E0401
21-
if _is_64bit_windows():
22-
key_name = 'SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VS7'
23-
else:
24-
key_name = 'SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VC7'
25-
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_name)
26-
i = 0
27-
vs_installs = [] # list of all Visual Studio installations
28-
while True:
29-
try:
30-
version, location, _ = winreg.EnumValue(key, i)
31-
vs_installs.append((version, location))
32-
except WindowsError: # pylint: disable=E0602
33-
break
34-
i += 1
35-
3619
# if a custom location is given, try that first
3720
if 'TREELITE_VCVARSALL' in os.environ:
3821
candidate = os.environ['TREELITE_VCVARSALL']
@@ -44,14 +27,52 @@ def _varsall_bat_path():
4427
raise OSError('Environment variable TREELITE_VCVARSALL does not refer '+\
4528
'to existing vcvarsall.bat')
4629

47-
# scan all detected Visual Studio installations, with most recent first
48-
for version, vcroot in sorted(vs_installs, key=lambda x: x[0], reverse=True):
49-
if version == '15.0': # Visual Studio 2017 revamped directory structure
50-
candidate = os.path.join(vcroot, 'VC\\Auxiliary\\Build\\vcvarsall.bat')
30+
## Bunch of heuristics to locate vcvarsall.bat
31+
candidate_paths = [] # List of possible paths to vcvarsall.bat
32+
try:
33+
import winreg # pylint: disable=E0401
34+
if _is_64bit_windows():
35+
key_name = 'SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\SxS\\VS7'
5136
else:
52-
candidate = os.path.join(vcroot, 'VC\\vcvarsall.bat')
37+
key_name = 'SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VC7'
38+
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_name)
39+
i = 0
40+
while True:
41+
try:
42+
version, vcroot, _ = winreg.EnumValue(key, i)
43+
if StrictVersion(version) >= StrictVersion('15.0'):
44+
# Visual Studio 2017 revamped directory structure
45+
candidate_paths.append(os.path.join(vcroot, 'VC\\Auxiliary\\Build\\vcvarsall.bat'))
46+
else:
47+
candidate_paths.append(os.path.join(vcroot, 'VC\\vcvarsall.bat'))
48+
except WindowsError: # pylint: disable=E0602
49+
break
50+
i += 1
51+
except FileNotFoundError:
52+
pass # No registry key found
53+
except ImportError:
54+
pass # No winreg module
55+
56+
for candidate in candidate_paths:
5357
if os.path.isfile(candidate):
5458
return candidate
59+
60+
# If registry method fails, try a bunch of pre-defined paths
61+
62+
# Visual Studio 2017 and higher
63+
for vcroot in glob.glob('C:\\Program Files (x86)\\Microsoft Visual Studio\\*') + \
64+
glob.glob('C:\\Program Files\\Microsoft Visual Studio\\*'):
65+
if re.fullmatch(r'[0-9]+', os.path.basename(vcroot)):
66+
for candidate in glob.glob(vcroot + '\\*\\VC\\Auxiliary\\Build\\vcvarsall.bat'):
67+
if os.path.isfile(candidate):
68+
return candidate
69+
# Previous versions of Visual Studio
70+
pattern = '\\Microsoft Visual Studio*\\VC\\vcvarsall.bat'
71+
for candidate in glob.glob('C:\\Program Files (x86)' + pattern) + \
72+
glob.glob('C:\\Program Files' + pattern):
73+
if os.path.isfile(candidate):
74+
return candidate
75+
5576
raise OSError('vcvarsall.bat not found; please specify its full path in '+\
5677
'the environment variable TREELITE_VCVARSALL')
5778

python/treelite/frontend.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import collections
66
import shutil
77
import os
8+
from tempfile import TemporaryDirectory
89
from .common.compat import STRING_TYPES
9-
from .common.util import c_str, TreeliteError, TemporaryDirectory
10+
from .common.util import c_str, TreeliteError
1011
from .core import _LIB, c_array, _check_call
1112
from .contrib import create_shared, generate_makefile, _toolchain_exist_check
1213

@@ -124,7 +125,7 @@ def export_lib(self, toolchain, libpath, params=None, compiler='ast_native',
124125
shutil.move('/temporary/directory/mymodel.dll', './mymodel.dll')
125126
"""
126127
_toolchain_exist_check(toolchain)
127-
with TemporaryDirectory() as temp_dir:
128+
with TemporaryDirectory(dir=os.path.dirname(libpath)) as temp_dir:
128129
self.compile(temp_dir, params, compiler, verbose)
129130
temp_libpath = create_shared(toolchain, temp_dir, nthread,
130131
verbose, options)

src/compiler/ast_native.cc

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -381,15 +381,32 @@ class ASTNativeCompiler : public Compiler {
381381
}
382382
array_th_len = formatter.str();
383383
}
384-
PrependToBuffer(dest,
385-
fmt::format(native::qnode_template,
386-
"array_threshold"_a = array_threshold,
387-
"array_th_begin"_a = array_th_begin,
388-
"array_th_len"_a = array_th_len,
389-
"total_num_threshold"_a = total_num_threshold), 0);
390-
AppendToBuffer(dest,
391-
fmt::format(native::quantize_loop_template,
392-
"num_feature"_a = num_feature_), indent);
384+
if (!array_threshold.empty() && !array_th_begin.empty() && !array_th_len.empty()) {
385+
PrependToBuffer(dest,
386+
fmt::format(native::qnode_template,
387+
"total_num_threshold"_a = total_num_threshold), 0);
388+
AppendToBuffer(dest,
389+
fmt::format(native::quantize_loop_template,
390+
"num_feature"_a = num_feature_), indent);
391+
}
392+
if (!array_threshold.empty()) {
393+
PrependToBuffer(dest,
394+
fmt::format("static const double threshold[] = {{\n"
395+
"{array_threshold}\n"
396+
"}};\n", "array_threshold"_a = array_threshold), 0);
397+
}
398+
if (!array_th_begin.empty()) {
399+
PrependToBuffer(dest,
400+
fmt::format("static const int th_begin[] = {{\n"
401+
"{array_th_begin}\n"
402+
"}};\n", "array_th_begin"_a = array_th_begin), 0);
403+
}
404+
if (!array_th_len.empty()) {
405+
PrependToBuffer(dest,
406+
fmt::format("static const int th_len[] = {{\n"
407+
"{array_th_len}\n"
408+
"}};\n", "array_th_len"_a = array_th_len), 0);
409+
}
393410
CHECK_EQ(node->children.size(), 1);
394411
WalkAST(node->children[0], dest, indent);
395412
}
@@ -424,28 +441,50 @@ class ASTNativeCompiler : public Compiler {
424441
[this](const OutputNode* node) { return RenderOutputStatement(node); },
425442
&array_nodes, &array_cat_bitmap, &array_cat_begin,
426443
&output_switch_statement, &common_comp_op);
444+
if (!array_nodes.empty()) {
445+
AppendToBuffer("header.h",
446+
fmt::format("extern const struct Node {node_array_name}[];\n",
447+
"node_array_name"_a = node_array_name), 0);
448+
AppendToBuffer("arrays.c",
449+
fmt::format("const struct Node {node_array_name}[] = {{\n"
450+
"{array_nodes}\n"
451+
"}};\n",
452+
"node_array_name"_a = node_array_name,
453+
"array_nodes"_a = array_nodes), 0);
454+
}
455+
456+
if (!array_cat_bitmap.empty()) {
457+
AppendToBuffer("header.h",
458+
fmt::format("extern const uint64_t {cat_bitmap_name}[];\n",
459+
"cat_bitmap_name"_a = cat_bitmap_name), 0);
460+
AppendToBuffer("arrays.c",
461+
fmt::format("const uint64_t {cat_bitmap_name}[] = {{\n"
462+
"{array_cat_bitmap}\n"
463+
"}};\n",
464+
"cat_bitmap_name"_a = cat_bitmap_name,
465+
"array_cat_bitmap"_a = array_cat_bitmap), 0);
466+
}
467+
468+
if (!array_cat_begin.empty()) {
469+
AppendToBuffer("header.h",
470+
fmt::format("extern const size_t {cat_begin_name}[];\n",
471+
"cat_begin_name"_a = cat_begin_name), 0);
472+
AppendToBuffer("arrays.c",
473+
fmt::format("const size_t {cat_begin_name}[] = {{\n"
474+
"{array_cat_begin}\n"
475+
"}};\n",
476+
"cat_begin_name"_a = cat_begin_name,
477+
"array_cat_begin"_a = array_cat_begin), 0);
478+
}
427479

428-
AppendToBuffer("header.h",
429-
fmt::format(native::code_folder_arrays_declaration_template,
430-
"node_array_name"_a = node_array_name,
431-
"cat_bitmap_name"_a = cat_bitmap_name,
432-
"cat_begin_name"_a = cat_begin_name), 0);
433-
AppendToBuffer("arrays.c",
434-
fmt::format(native::code_folder_arrays_template,
435-
"node_array_name"_a = node_array_name,
436-
"array_nodes"_a = array_nodes,
437-
"cat_bitmap_name"_a = cat_bitmap_name,
438-
"array_cat_bitmap"_a = array_cat_bitmap,
439-
"cat_begin_name"_a = cat_begin_name,
440-
"array_cat_begin"_a = array_cat_begin), 0);
441480
if (array_nodes.empty()) {
442481
/* folded code consists of a single leaf node */
443482
AppendToBuffer(dest,
444483
fmt::format("nid = -1;\n"
445484
"{output_switch_statement}\n",
446485
"output_switch_statement"_a
447486
= output_switch_statement), indent);
448-
} else {
487+
} else if (!array_cat_bitmap.empty() && !array_cat_begin.empty()) {
449488
AppendToBuffer(dest,
450489
fmt::format(native::eval_loop_template,
451490
"node_array_name"_a = node_array_name,
@@ -455,6 +494,14 @@ class ASTNativeCompiler : public Compiler {
455494
"comp_op"_a = OpName(common_comp_op),
456495
"output_switch_statement"_a
457496
= output_switch_statement), indent);
497+
} else {
498+
AppendToBuffer(dest,
499+
fmt::format(native::eval_loop_template_without_categorical_feature,
500+
"node_array_name"_a = node_array_name,
501+
"data_field"_a = (param.quantize > 0 ? "qvalue" : "fvalue"),
502+
"comp_op"_a = OpName(common_comp_op),
503+
"output_switch_statement"_a
504+
= output_switch_statement), indent);
458505
}
459506
}
460507

0 commit comments

Comments
 (0)