Marcel Telka
2024-04-08 d6ccb6ab62f2f8726859d4d567e79af34435042f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
# Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
#
 
#
# The Python build infrastructure in setup.py.mk and pyproject.mk files uses
# several Python projects to work properly.  Since we cannot use these projects
# until they are actually built and installed we need to bootstrap.
#
# We do have several sequential bootstrap checkpoints during the process:
#
# (0)    Nothing works yet.
#
#     Just core Python runtime is available (with no additional projects).
#     While here almost nothing works.  We cannot do following tasks with
#     regular Python projects:
#         - detect their requirements,
#         - build and publish them,
#         - test them.
#
# (1)     The bootstrapper is ready.
#
#     The bootstrapper is special tool that requires just core Python with no
#     dependency on other Python projects and it is able to build and publish
#     itself and other Python projects.
#
#     For projects using the 'setup.py' build style we do not need any
#     special bootstrapper because such projects are built using their own
#     'setup.py' script.  The only issue with the 'setup.py' build style
#     projects is that their 'setup.py' script usually depends on some other
#     projects (typically setuptools) to get successfully built.
#
#     For 'pyproject'-style projects we use pyproject_installer as the
#     bootstrapper.
#
#     To achieve this checkpoint we just need to build pyproject_installer
#     using pyproject_installer without detecting its requirements (they are
#     none anyway) and without testing it (since no testing infrastructure is
#     ready yet).
#
# (2)    The python-requires script works.
#
#     Once the python-requires script works we can start to detect runtime
#     dependencie of other Python projects automatically.
#
#     To achieve this checkpoint we need to build the packaging project
#     (directly needed by the python-requires script) and all projects
#     required by packaging.  During this all projects' dependencies needs to
#     be manually evaluated to make sure they are correct.
#
# (3)    The build infrastructure is fully working.
#
#    Once we are here we can build any Python project, but we cannot test it
#    yet.
#
#     For projects using the 'setup.py' build style we do not need any
#     special build infrastructure.  See checkpoint (1) above for detialed
#     discussion about 'setup.py' build style projects.
#
#     For 'pyproject'-style projects we need to build both 'build' and
#     'installer' projects and all projects they depends on.
#
# (4)    The testing infrastructure is fully working.
#
#     Once we are here we can finally use all features of the Python build
#     framework.  Including testing.
#
#     To achieve this we need to build tox, tox-current-env, and pytest
#     projects together with their dependencies.
#
# All projects needed to achieve checkpoints (1), (2), and (3) should set
# PYTHON_BOOTSTRAP to 'yes' in their Makefile to make sure the regular build
# infrastructure is not used for them and special set of build rules is applied
# instead.
#
# All projects needed to go from checkpoint (3) to checkpoint (4) should set
# PYTHON_TEST_BOOTSTRAP to 'yes' in their Makefile to let the build
# infrastructure know that testing for such projects might not work properly.
#
# The PYTHON_BOOTSTRAP set to 'yes' implies PYTHON_TEST_BOOTSTRAP set to 'yes'
# too.
#
ifeq ($(strip $(PYTHON_BOOTSTRAP)),yes)
PYTHON_TEST_BOOTSTRAP = yes
endif
 
#
# Lists of Python projects needed to achieve particular bootstrap checkpoint.
# Indentation shows project dependencies (e.g. packaging requires flit_core).
#
PYTHON_BOOTSTRAP_CHECKPOINT_1 +=    pyproject_installer
#
PYTHON_BOOTSTRAP_CHECKPOINT_2 +=    $(PYTHON_BOOTSTRAP_CHECKPOINT_1)
PYTHON_BOOTSTRAP_CHECKPOINT_2 +=    packaging
PYTHON_BOOTSTRAP_CHECKPOINT_2 +=        flit_core
 
# Particular python runtime is always required (at least to run setup.py)
PYTHON_REQUIRED_PACKAGES += runtime/python
 
define python-rule
$(BUILD_DIR)/%-$(1)/.built:        PYTHON_VERSION=$(1)
$(BUILD_DIR)/%-$(1)/.installed:        PYTHON_VERSION=$(1)
$(BUILD_DIR)/%-$(1)/.tested:        PYTHON_VERSION=$(1)
$(BUILD_DIR)/%-$(1)/.tested-and-compared:    PYTHON_VERSION=$(1)
endef
 
$(foreach pyver, $(PYTHON_VERSIONS), $(eval $(call python-rule,$(pyver))))
 
$(BUILD_DIR)/$(MACH32)-%/.built:    BITS=32
$(BUILD_DIR)/$(MACH64)-%/.built:    BITS=64
$(BUILD_DIR)/$(MACH32)-%/.installed:    BITS=32
$(BUILD_DIR)/$(MACH64)-%/.installed:    BITS=64
$(BUILD_DIR)/$(MACH32)-%/.tested:    BITS=32
$(BUILD_DIR)/$(MACH64)-%/.tested:    BITS=64
$(BUILD_DIR)/$(MACH32)-%/.tested-and-compared:    BITS=32
$(BUILD_DIR)/$(MACH64)-%/.tested-and-compared:    BITS=64
 
PYTHON_32_VERSIONS = $(filter-out $(PYTHON_64_ONLY_VERSIONS), $(PYTHON_VERSIONS))
 
BUILD_32 = $(PYTHON_32_VERSIONS:%=$(BUILD_DIR)/$(MACH32)-%/.built)
BUILD_64 = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH64)-%/.built)
BUILD_NO_ARCH = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH)-%/.built)
 
ifeq ($(filter-out $(PYTHON_64_ONLY_VERSIONS), $(PYTHON_VERSION)),)
BUILD_32_and_64 = $(BUILD_64)
endif
 
INSTALL_32 = $(PYTHON_32_VERSIONS:%=$(BUILD_DIR)/$(MACH32)-%/.installed)
INSTALL_64 = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH64)-%/.installed)
INSTALL_NO_ARCH = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH)-%/.installed)
 
PYTHON_ENV =    CC="$(CC)"
PYTHON_ENV +=    CFLAGS="$(CFLAGS)"
PYTHON_ENV +=    CXX="$(CXX)"
PYTHON_ENV +=    CXXFLAGS="$(CXXFLAGS)"
PYTHON_ENV +=    LDFLAGS="$(LDFLAGS)"
PYTHON_ENV +=    PKG_CONFIG_PATH="$(PKG_CONFIG_PATH)"
 
COMPONENT_BUILD_ENV += $(PYTHON_ENV)
COMPONENT_INSTALL_ENV += $(PYTHON_ENV)
COMPONENT_TEST_ENV += $(PYTHON_ENV)
 
# Set CARGO_HOME to make sure projects built using rust (for example via
# setuptools-rust) do not pollute user's home directory with cargo bits.
COMPONENT_BUILD_ENV += CARGO_HOME=$(@D)/.cargo
 
# Make sure the default Python version is installed last and so is the
# canonical version.  This is needed for components that keep PYTHON_VERSIONS
# set to more than single value, but deliver unversioned binaries in usr/bin or
# other overlapping files.
define python-order-rule
$(BUILD_DIR)/%-$(PYTHON_VERSION)/.installed:    $(BUILD_DIR)/%-$(1)/.installed
endef
$(foreach pyver,$(filter-out $(PYTHON_VERSION),$(PYTHON_VERSIONS)),$(eval $(call python-order-rule,$(pyver))))
 
# We need to copy the source dir to avoid its modification by install target
# where egg-info is re-generated
CLONEY_ARGS = CLONEY_MODE="copy"
 
COMPONENT_BUILD_CMD = $(PYTHON) setup.py --no-user-cfg build $(COMPONENT_BUILD_SETUP_PY_ARGS)
 
# build the configured source
$(BUILD_DIR)/%/.built:    $(SOURCE_DIR)/.prep
    $(RM) -r $(@D) ; $(MKDIR) $(@D)
    $(ENV) $(CLONEY_ARGS) $(CLONEY) $(SOURCE_DIR) $(@D)
    $(COMPONENT_PRE_BUILD_ACTION)
    (cd $(@D)$(COMPONENT_SUBDIR:%=/%) ; $(ENV) $(COMPONENT_BUILD_ENV) \
        $(COMPONENT_BUILD_CMD) $(COMPONENT_BUILD_ARGS))
    $(COMPONENT_POST_BUILD_ACTION)
    $(TOUCH) $@
 
 
COMPONENT_INSTALL_CMD = $(PYTHON) setup.py --no-user-cfg install
 
COMPONENT_INSTALL_ARGS +=    --root $(PROTO_DIR) 
COMPONENT_INSTALL_ARGS +=    --install-lib=$(PYTHON_LIB)
COMPONENT_INSTALL_ARGS +=    --install-data=$(PYTHON_DATA)
COMPONENT_INSTALL_ARGS +=    --skip-build
COMPONENT_INSTALL_ARGS +=    --force
 
# install the built source into a prototype area
$(BUILD_DIR)/%/.installed:    $(BUILD_DIR)/%/.built
    $(COMPONENT_PRE_INSTALL_ACTION)
    (cd $(@D)$(COMPONENT_SUBDIR:%=/%) ; $(ENV) $(COMPONENT_INSTALL_ENV) \
        $(COMPONENT_INSTALL_CMD) $(COMPONENT_INSTALL_ARGS))
    $(COMPONENT_POST_INSTALL_ACTION)
    $(TOUCH) $@
 
ifeq ($(strip $(SINGLE_PYTHON_VERSION)),no)
# Rename binaries in /usr/bin to contain version number
COMPONENT_POST_INSTALL_ACTION += \
    for f in $(PROTOUSRBINDIR)/* ; do \
        [ -f $$f ] || continue ; \
        for v in $(PYTHON_VERSIONS) ; do \
            [ "$$f" == "$${f%%$$v}" ] || continue 2 ; \
        done ; \
        $(MV) $$f $$f-$(PYTHON_VERSION) ; \
    done ;
endif
 
# Remove any previous dependency files
COMPONENT_POST_INSTALL_ACTION +=    $(RM) $(@D)/.depend-runtime $(@D)/.depend-test ;
 
# Define Python version specific filenames for tests.
ifeq ($(strip $(USE_COMMON_TEST_MASTER)),no)
COMPONENT_TEST_MASTER =    $(COMPONENT_TEST_RESULTS_DIR)/results-$(PYTHON_VERSION).master
endif
COMPONENT_TEST_BUILD_DIR = $(BUILD_DIR)/test-$(PYTHON_VERSION)
COMPONENT_TEST_OUTPUT =    $(COMPONENT_TEST_BUILD_DIR)/test-$(PYTHON_VERSION)-results
COMPONENT_TEST_DIFFS =    $(COMPONENT_TEST_BUILD_DIR)/test-$(PYTHON_VERSION)-diffs
COMPONENT_TEST_SNAPSHOT = $(COMPONENT_TEST_BUILD_DIR)/results-$(PYTHON_VERSION).snapshot
COMPONENT_TEST_TRANSFORM_CMD = $(COMPONENT_TEST_BUILD_DIR)/transform-$(PYTHON_VERSION)-results
 
# Generic transforms for Python test results.
# See below for test style specific transforms.
COMPONENT_TEST_TRANSFORMS += "-e 's|$(PYTHON_DIR)|\$$(PYTHON_DIR)|g'"
 
# Testing depends on install target because we want to test installed modules
COMPONENT_TEST_DEP +=    $(BUILD_DIR)/%/.installed
# Point Python to the proto area so it is able to find installed modules there
COMPONENT_TEST_ENV +=    PYTHONPATH=$(PROTO_DIR)/$(PYTHON_LIB)
# Make sure testing is able to find own installed executables (if any)
COMPONENT_TEST_ENV +=    PATH=$(PROTOUSRBINDIR):$(PATH)
 
# determine the type of tests we want to run.
ifeq ($(strip $(wildcard $(COMPONENT_TEST_RESULTS_DIR)/results-*.master)),)
TEST_32 = $(PYTHON_32_VERSIONS:%=$(BUILD_DIR)/$(MACH32)-%/.tested)
TEST_64 = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH64)-%/.tested)
TEST_NO_ARCH = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH)-%/.tested)
else
TEST_32 = $(PYTHON_32_VERSIONS:%=$(BUILD_DIR)/$(MACH32)-%/.tested-and-compared)
TEST_64 = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH64)-%/.tested-and-compared)
TEST_NO_ARCH = $(PYTHON_VERSIONS:%=$(BUILD_DIR)/$(MACH)-%/.tested-and-compared)
endif
 
#
# Testing in the Python world is complex.  Python projects usually do not
# support Makefile with common 'check' or 'test' target to get built bits
# tested.
#
# De facto standard way to test Python projects these days is tox which is
# designed and used primarily for release testing; to make sure the released
# python project runs on all supported Python versions, platforms, etc.  tox
# does so using virtualenv and creates isolated test environments where the
# tested package together with all its dependencies is automatically installed
# (using pip) and tested.  This is great for Python projects developers but it
# is hardly usable for operating system distributions like OpenIndiana.
#
# We do not need such release testing.  Instead we need something closer to
# integration testing: we need to test the built component in our real
# environment without automatic installation of any dependencies using pip.  In
# addition, we need to run tests only for Python versions we actually support
# and the component is built for.
#
# To achieve that we do few things.  First, to avoid isolated environments
# (virtualenv) we run tox with the tox-current-env plugin.  Second, to test
# only Python versions we are interested in we use -e option for tox to select
# single Python version only.  Since we run separate test target per Python
# version this will make sure we test all needed Python versions.
#
# The tox tool itself uses some other tools under the hood to run tests, for
# example pytest.  Some projects could even support pytest testing directly
# without support for tox.  For such projects we offer separate "pytest"-style
# testing.
#
# For projects that do not support testing using neither tox nor pytest we
# offer either unittest or (deprecated) "setup.py test" testing too.
#
# The TEST_STYLE variable is used to select (or force) particular test style
# for Python projects.  Valid values are:
#
#     tox        - "tox"-style testing
#     pytest        - "pytest"-style testing
#     unittest    - "unittest"-style testing
#     setup.py    - "setup.py test"-style testing
#     none        - no testing is supported (or desired) at all
#
 
TEST_STYLE ?= tox
ifeq ($(strip $(TEST_STYLE)),tox)
# tox needs PATH environment variable - see https://github.com/tox-dev/tox/issues/2538
# We already added it to the test environment - see above
COMPONENT_TEST_ENV +=        PYTEST_ADDOPTS="$(PYTEST_ADDOPTS)"
COMPONENT_TEST_ENV +=        NOSE_VERBOSE=2
COMPONENT_TEST_CMD =        $(TOX)
COMPONENT_TEST_ARGS =        --current-env --no-provision
COMPONENT_TEST_ARGS +=        --recreate
COMPONENT_TEST_ARGS +=        $(TOX_TESTENV)
COMPONENT_TEST_TARGETS =    $(if $(strip $(TOX_POSARGS)),-- $(TOX_POSARGS))
 
TOX_TESTENV = -e py$(subst .,,$(PYTHON_VERSION))
 
# Make sure following tools are called indirectly to properly support tox-current-env
TOX_CALL_INDIRECTLY += py.test
TOX_CALL_INDIRECTLY += pytest
TOX_CALL_INDIRECTLY += coverage
TOX_CALL_INDIRECTLY += zope-testrunner
TOX_CALL_INDIRECTLY.zope-testrunner = zope.testrunner
TOX_CALL_INDIRECTLY += sphinx-build
TOX_CALL_INDIRECTLY.sphinx-build = sphinx.cmd.build
TOX_CALL_INDIRECTLY += nosetests
TOX_CALL_INDIRECTLY.nosetests = nose
$(foreach indirectly, $(TOX_CALL_INDIRECTLY), $(eval TOX_CALL_INDIRECTLY.$(indirectly) ?= $(indirectly)))
COMPONENT_PRE_TEST_ACTION += COMPONENT_TEST_DIR=$(COMPONENT_TEST_DIR) ;
COMPONENT_PRE_TEST_ACTION += \
    $(foreach indirectly, $(TOX_CALL_INDIRECTLY), \
        [ -f $$COMPONENT_TEST_DIR/tox.ini ] && \
            $(GSED) -i -e '/^commands *=/,/^$$/{ \
                s/^\(\(commands *=\)\{0,1\}[ \t]*\)'$(indirectly)'\([ \t]\{1,\}.*\)\{0,1\}$$/\1python -m '$(TOX_CALL_INDIRECTLY.$(indirectly))'\3/ \
            }' $$COMPONENT_TEST_DIR/tox.ini ; \
    )
COMPONENT_PRE_TEST_ACTION += true ;
 
# Normalize tox test results.
COMPONENT_TEST_TRANSFORMS += "-e 's/py$(subst .,,$(PYTHON_VERSION))/py\$$(PYV)/g'"    # normalize PYV
COMPONENT_TEST_TRANSFORMS += "-e '/^py\$$(PYV) installed:/d'"        # depends on set of installed packages
COMPONENT_TEST_TRANSFORMS += "-e '/PYTHONHASHSEED/d'"            # this is random
 
# Normalize zope.testrunner test results
COMPONENT_TEST_TRANSFORMS += \
    "-e 's/ in \([0-9]\{1,\} minutes \)\{0,1\}[0-9]\{1,\}\.[0-9]\{3\} seconds//'"    # timing
 
# Remove timing for tox 4 test results
COMPONENT_TEST_TRANSFORMS += "-e 's/^\(  py\$$(PYV): OK\) (.* seconds)$$/\1/'"
COMPONENT_TEST_TRANSFORMS += "-e 's/^\(  congratulations :)\) (.* seconds)$$/\1/'"
 
# Remove useless lines from the "coverage combine" output
COMPONENT_TEST_TRANSFORMS += "-e '/^Combined data file .*\.coverage/d'"
COMPONENT_TEST_TRANSFORMS += "-e '/^Skipping duplicate data .*\.coverage/d'"
 
# sort list of Sphinx doctest results
COMPONENT_TEST_TRANSFORMS += \
    "| ( \
        $(GSED) -u -e '/^running tests\.\.\.$$/q' ; \
        $(GSED) -u -e '/^Doctest summary/Q' \
            | $(NAWK) '/^$$/{\$$0=\"\\\\n\"}1' ORS='|' \
            | $(GNU_GREP) -v '^|$$' \
            | $(SORT) \
            | tr -d '\\\\n' | tr '|' '\\\\n' \
            | $(NAWK) '{print}END{if(NR>0)printf(\"\\\\nDoctest summary\\\\n\")}' ; \
        $(CAT) \
    ) | $(COMPONENT_TEST_TRANSFORMER)"
 
# tox package together with the tox-current-env plugin is needed
USERLAND_TEST_REQUIRED_PACKAGES += library/python/tox
USERLAND_TEST_REQUIRED_PACKAGES += library/python/tox-current-env
 
# Generate raw lists of test dependencies per Python version
# Please note we set PATH below four times for $(COMPONENT_TEST_CMD) (aka tox)
# to workaround https://github.com/tox-dev/tox/issues/2538
COMPONENT_POST_INSTALL_ACTION += \
    if [ -x "$(COMPONENT_TEST_CMD)" ] ; then \
        cd $(@D)$(COMPONENT_SUBDIR:%=/%) ; \
        echo "Testing dependencies:" ; \
        PATH=$(PATH) $(COMPONENT_TEST_CMD) -qq --no-provision --print-deps-to=- $(TOX_TESTENV) || exit 1 ; \
        echo "Testing extras:" ; \
        PATH=$(PATH) $(COMPONENT_TEST_CMD) -qq --no-provision --print-extras-to=- $(TOX_TESTENV) || exit 1 ; \
        ( PATH=$(PATH) $(COMPONENT_TEST_CMD) -qq --no-provision --print-deps-to=- $(TOX_TESTENV) \
            | $(WS_TOOLS)/python-resolve-deps \
                PYTHONPATH=$(PROTO_DIR)/$(PYTHON_DIR)/site-packages:$(PROTO_DIR)/$(PYTHON_LIB) \
                $(PYTHON) $(WS_TOOLS)/python-requires $(COMPONENT_NAME) \
            | $(PYTHON) $(WS_TOOLS)/python-requires - ; \
        for e in $$(PATH=$(PATH) $(COMPONENT_TEST_CMD) -qq --no-provision --print-extras-to=- $(TOX_TESTENV)) ; do \
            PYTHONPATH=$(PROTO_DIR)/$(PYTHON_DIR)/site-packages:$(PROTO_DIR)/$(PYTHON_LIB) \
                $(PYTHON) $(WS_TOOLS)/python-requires $(COMPONENT_NAME) $$e ; \
        done ) | $(GSED) -e '/^tox\(-current-env\)\?$$/d' >> $(@D)/.depend-test ; \
    fi ;
else ifeq ($(strip $(TEST_STYLE)),pytest)
COMPONENT_TEST_CMD =        $(PYTHON) -m pytest
COMPONENT_TEST_ARGS =        $(PYTEST_ADDOPTS)
COMPONENT_TEST_TARGETS =
 
USERLAND_TEST_REQUIRED_PACKAGES += library/python/pytest
else ifeq ($(strip $(TEST_STYLE)),unittest)
COMPONENT_TEST_CMD =        $(PYTHON) -m unittest
COMPONENT_TEST_ARGS =
COMPONENT_TEST_ARGS +=        --verbose
COMPONENT_TEST_TARGETS =
else ifeq ($(strip $(TEST_STYLE)),setup.py)
# Old and deprecated "setup.py test"-style testing
COMPONENT_TEST_CMD =        $(PYTHON) setup.py
COMPONENT_TEST_ARGS =        --no-user-cfg
COMPONENT_TEST_TARGETS =    test
else ifeq ($(strip $(TEST_STYLE)),none)
TEST_TARGET = $(NO_TESTS)
endif
 
# Run pytest verbose to get separate line per test in results output
PYTEST_ADDOPTS += --verbose
 
# Force pytest to not use colored output so the results normalization is unaffected
PYTEST_ADDOPTS += --color=no
 
# Avoid loading of unexpected pytest plugins.
define disable-pytest-plugin
PYTEST_ADDOPTS += $$(if $$(filter library/python/$(2)-$$(subst .,,$$(PYTHON_VERSION)), $$(REQUIRED_PACKAGES) $$(TEST_REQUIRED_PACKAGES) $$(COMPONENT_FMRI)-$$(subst .,,$$(PYTHON_VERSION))),,-p 'no:$(1)')
endef
$(eval $(call disable-pytest-plugin,anyio,anyio))
$(eval $(call disable-pytest-plugin,asyncio,pytest-asyncio))        # adds line to test report header
$(eval $(call disable-pytest-plugin,benchmark,pytest-benchmark))    # adds line to test report header; adds benchmark report
$(eval $(call disable-pytest-plugin,black,pytest-black))        # runs extra test(s)
$(eval $(call disable-pytest-plugin,check,pytest-check))
$(eval $(call disable-pytest-plugin,checkdocs,pytest-checkdocs))    # runs extra test(s)
$(eval $(call disable-pytest-plugin,console-scripts,pytest-console-scripts))
$(eval $(call disable-pytest-plugin,cov,pytest-cov))
$(eval $(call disable-pytest-plugin,custom_exit_code,pytest-custom-exit-code))
$(eval $(call disable-pytest-plugin,enabler,pytest-enabler))
$(eval $(call disable-pytest-plugin,env,pytest-env))
$(eval $(call disable-pytest-plugin,faker,faker))
$(eval $(call disable-pytest-plugin,flake8,pytest-flake8))
$(eval $(call disable-pytest-plugin,flaky,flaky))
$(eval $(call disable-pytest-plugin,freezer,pytest-freezer))
$(eval $(call disable-pytest-plugin,helpers_namespace,pytest-helpers-namespace))
$(eval $(call disable-pytest-plugin,hypothesispytest,hypothesis))    # adds line to test report header
$(eval $(call disable-pytest-plugin,jaraco.test.http,jaraco-test))
$(eval $(call disable-pytest-plugin,kgb,kgb))
$(eval $(call disable-pytest-plugin,metadata,pytest-metadata))        # adds line to test report header
$(eval $(call disable-pytest-plugin,mypy,pytest-mypy))            # runs extra test(s)
$(eval $(call disable-pytest-plugin,perf,pytest-perf))            # https://github.com/jaraco/pytest-perf/issues/9
$(eval $(call disable-pytest-plugin,pytest home,pytest-home))
$(eval $(call disable-pytest-plugin,pytest-datadir,pytest-datadir))
$(eval $(call disable-pytest-plugin,pytest-mypy-plugins,pytest-mypy-plugins))    # could cause tests to fail
$(eval $(call disable-pytest-plugin,pytest-teamcity,teamcity-messages))
$(eval $(call disable-pytest-plugin,pytest_expect,pytest-expect))
$(eval $(call disable-pytest-plugin,pytest_fakefs,pyfakefs))
$(eval $(call disable-pytest-plugin,pytest_forked,pytest-forked))
$(eval $(call disable-pytest-plugin,pytest_httpserver,pytest-httpserver))
$(eval $(call disable-pytest-plugin,pytest_ignore_flaky,pytest-ignore-flaky))
$(eval $(call disable-pytest-plugin,pytest_lazyfixture,pytest-lazy-fixtures))
$(eval $(call disable-pytest-plugin,pytest_mock,pytest-mock))
$(eval $(call disable-pytest-plugin,randomly,pytest-randomly))        # reorders tests
$(eval $(call disable-pytest-plugin,regressions,pytest-regressions))
$(eval $(call disable-pytest-plugin,relaxed,pytest-relaxed))        # runs extra test(s); produces different test report
$(eval $(call disable-pytest-plugin,reporter,pytest-reporter))        # https://github.com/christiansandberg/pytest-reporter/issues/8
$(eval $(call disable-pytest-plugin,rerunfailures,pytest-rerunfailures))
$(eval $(call disable-pytest-plugin,salt-factories,pytest-salt-factories))            # requires salt
$(eval $(call disable-pytest-plugin,salt-factories-event-listener,pytest-salt-factories))    # requires salt
$(eval $(call disable-pytest-plugin,salt-factories-factories,pytest-salt-factories))        # requires salt
$(eval $(call disable-pytest-plugin,salt-factories-loader-mock,pytest-salt-factories))        # requires salt
$(eval $(call disable-pytest-plugin,salt-factories-log-server,pytest-salt-factories))        # requires salt
$(eval $(call disable-pytest-plugin,salt-factories-markers,pytest-salt-factories))        # requires salt
$(eval $(call disable-pytest-plugin,salt-factories-sysinfo,pytest-salt-factories))        # requires salt
$(eval $(call disable-pytest-plugin,salt-factories-sysstats,pytest-salt-factories))        # requires salt
$(eval $(call disable-pytest-plugin,shell-utilities,pytest-shell-utilities))
$(eval $(call disable-pytest-plugin,skip-markers,pytest-skip-markers))
$(eval $(call disable-pytest-plugin,socket,pytest-socket))
$(eval $(call disable-pytest-plugin,subprocess,pytest-subprocess))
$(eval $(call disable-pytest-plugin,subtests,pytest-subtests))
$(eval $(call disable-pytest-plugin,system-statistics,pytest-system-statistics))
$(eval $(call disable-pytest-plugin,time_machine,time-machine))
$(eval $(call disable-pytest-plugin,timeout,pytest-timeout))
$(eval $(call disable-pytest-plugin,travis-fold,pytest-travis-fold))
$(eval $(call disable-pytest-plugin,typeguard,typeguard))
$(eval $(call disable-pytest-plugin,xdist,pytest-xdist))
$(eval $(call disable-pytest-plugin,xdist.looponfail,pytest-xdist))
$(eval $(call disable-pytest-plugin,xprocess,pytest-xprocess))        # adds a reminder line to test output
 
# By default we are not interested in full list of test failures so exit on
# first failure to save time.  This could be easily overridden from environment
# if needed (for example to debug test failures) or in per-component Makefile.
PYTEST_FASTFAIL = -x
PYTEST_ADDOPTS += $(PYTEST_FASTFAIL)
 
# By default we are not interested to see the default long tracebacks.
# Detailed tracebacks are shown either for failures or xfails.  We aim to see
# testing passed so there should be no failures.  Since xfails are expected
# failures we are not interested in detailed tracebacks here at all since they
# could contain random data, like pointers, temporary file names, etc.
PYTEST_TRACEBACK = --tb=line
PYTEST_ADDOPTS += $(PYTEST_TRACEBACK)
 
# Normalize pytest test results.  The pytest framework could be used either
# directly or via tox or setup.py so add these transforms for all test styles
# unconditionally.
COMPONENT_TEST_TRANSFORMS += \
    "-e 's/^\(platform sunos5 -- Python \)$(shell echo $(PYTHON_VERSION) | $(GSED) -e 's/\./\\./g')\.[0-9]\{1,\}.*\( -- .*\)/\1\$$(PYTHON_VERSION).X\2/'"
COMPONENT_TEST_TRANSFORMS += "-e '/^plugins: /d'"                # order of listed plugins could vary
COMPONENT_TEST_TRANSFORMS += "-e '/^-\{1,\} coverage: /,/^$$/d'"        # remove coverage report
COMPONENT_TEST_TRANSFORMS += "-e 's/ \{1,\}\[...%\]\$$//'"            # drop percentage
COMPONENT_TEST_TRANSFORMS += \
    "-e 's/^=\{1,\} \(.*\) in [0-9]\{1,\}\.[0-9]\{1,\}s \(([^)]*) \)\?=\{1,\}$$/======== \1 ========/'"    # remove timing
# Remove slowest durations report for projects that run pytest with --durations option
COMPONENT_TEST_TRANSFORMS += "-e '/^=\{1,\} slowest [0-9 ]*durations =\{1,\}$$/,/^=/{/^=/!d}'"
# Remove short test summary info for projects that run pytest with -r option
COMPONENT_TEST_TRANSFORMS += "-e '/^=\{1,\} short test summary info =\{1,\}$$/,/^=/{/^=/!d}'"
 
# Normalize test results produced by pytest-benchmark
COMPONENT_TEST_TRANSFORMS += \
    $(if $(filter library/python/pytest-benchmark-$(subst .,,$(PYTHON_VERSION)), $(REQUIRED_PACKAGES) $(TEST_REQUIRED_PACKAGES)),"| ( \
        $(GSED) -e '/^-\{1,\} benchmark/,/^=/{/^=/!d}' \
    ) | $(COMPONENT_TEST_TRANSFORMER) -e ''")
 
# Normalize test results produced by pytest-randomly
USE_PYTEST_RANDOMLY = $(filter library/python/pytest-randomly-$(subst .,,$(PYTHON_VERSION)), $(REQUIRED_PACKAGES) $(TEST_REQUIRED_PACKAGES))
PYTEST_SORT_TESTS = $(USE_PYTEST_RANDOMLY)
COMPONENT_TEST_TRANSFORMS += $(if $(strip $(USE_PYTEST_RANDOMLY)),"-e '/^Using --randomly-seed=[0-9]\{1$(comma)\}\$$/d'")
COMPONENT_TEST_TRANSFORMS += \
    $(if $(strip $(PYTEST_SORT_TESTS)),"| ( \
        $(GSED) -u -e '/^=\{1$(comma)\} test session starts /q' ; \
        $(GSED) -u -e '/^\$$/q' ; \
        $(GSED) -u -e '/^\$$/Q' | $(SORT) | $(GSED) -e '\$$a\'\$$'\\\n\\\n' ; \
        $(CAT) \
    ) | $(COMPONENT_TEST_TRANSFORMER) -e ''")
 
# Normalize test results produced by pytest-xdist
COMPONENT_TEST_TRANSFORMS += \
    $(if $(filter library/python/pytest-xdist-$(subst .,,$(PYTHON_VERSION)), $(REQUIRED_PACKAGES) $(TEST_REQUIRED_PACKAGES)),"| ( \
        $(GSED) -u \
            -e '/^created: .* workers$$/d' \
            -e 's/^[0-9]\{1,\}\( workers \[[0-9]\{1,\} items\]\)$$/X\1/' \
            -e '/^scheduling tests via /q' ; \
        $(GSED) -u -e '/^$$/q' ; \
        $(GSED) -u -n -e '/^\[gw/p' -e '/^$$/Q' | ( $(GSED) \
            -e 's/^\[gw[0-9]\{1,\}\] \[...%\] //' \
            -e 's/ *$$//' \
            -e 's/\([^ ]\{1,\}\) \(.*\)$$/\2 \1/' \
            | $(SORT) | $(NAWK) '{print}END{if(NR>0)printf(\"\\\\n\")}' ; \
        ) ; \
        $(CAT) \
    ) | $(COMPONENT_TEST_TRANSFORMER) -e ''")
 
# Normalize stestr test results
USE_STESTR = $(filter library/python/stestr-$(subst .,,$(PYTHON_VERSION)), $(REQUIRED_PACKAGES) $(TEST_REQUIRED_PACKAGES))
COMPONENT_TEST_TRANSFORMS += \
    $(if $(strip $(USE_STESTR)),"| ( \
            $(GSED) -e '0,/^{[0-9]\{1,\}}/{//i\'\$$'\\\n{0}\\\n}' \
                -e 's/^\(Ran: [0-9]\{1,\} tests\{0,1\}\) in .*\$$/\1/' \
                -e '/^Sum of execute time for each test/d' \
                -e '/^ - Worker /d' \
        ) | ( \
            $(GSED) -u -e '/^{0}\$$/Q' ; \
            $(GSED) -u -e 's/^{[0-9]\{1,\}} //' \
                -e 's/\[[.0-9]\{1,\}s\] \.\.\./.../' \
                -e '/^\$$/Q' | $(SORT) | $(GSED) -e '\$$a\'\$$'\\\n\\\n' ; \
            $(CAT) \
        ) | $(COMPONENT_TEST_TRANSFORMER) -e ''")
 
# Normalize setup.py test results.  The setup.py testing could be used either
# directly or via tox so add these transforms for all test styles
# unconditionally.
COMPONENT_TEST_TRANSFORMS += "-e '/SetuptoolsDeprecationWarning:/,+1d'"        # depends on Python version and is useless
COMPONENT_TEST_TRANSFORMS += "-e 's/^\(Ran [0-9]\{1,\} tests\{0,1\}\) in .*$$/\1/'"    # delete timing from test results
 
COMPONENT_TEST_DIR = $(@D)$(COMPONENT_SUBDIR:%=/%)
 
# test the built source
$(BUILD_DIR)/%/.tested-and-compared:    $(COMPONENT_TEST_DEP)
    $(RM) -rf $(COMPONENT_TEST_BUILD_DIR)
    $(MKDIR) $(COMPONENT_TEST_BUILD_DIR)
    $(COMPONENT_PRE_TEST_ACTION)
    -(cd $(COMPONENT_TEST_DIR) ; \
        $(COMPONENT_TEST_ENV_CMD) $(COMPONENT_TEST_ENV) \
        $(COMPONENT_TEST_CMD) \
        $(COMPONENT_TEST_ARGS) $(COMPONENT_TEST_TARGETS)) \
        &> $(COMPONENT_TEST_OUTPUT)
    $(COMPONENT_POST_TEST_ACTION)
    $(COMPONENT_TEST_CREATE_TRANSFORMS)
    $(COMPONENT_TEST_PERFORM_TRANSFORM)
    $(COMPONENT_TEST_COMPARE)
    $(COMPONENT_TEST_CLEANUP)
    $(TOUCH) $@
 
$(BUILD_DIR)/%/.tested:    SHELLOPTS=pipefail
$(BUILD_DIR)/%/.tested:    $(COMPONENT_TEST_DEP)
    $(RM) -rf $(COMPONENT_TEST_BUILD_DIR)
    $(MKDIR) $(COMPONENT_TEST_BUILD_DIR)
    $(COMPONENT_PRE_TEST_ACTION)
    (cd $(COMPONENT_TEST_DIR) ; \
        $(COMPONENT_TEST_ENV_CMD) $(COMPONENT_TEST_ENV) \
        $(COMPONENT_TEST_CMD) \
        $(COMPONENT_TEST_ARGS) $(COMPONENT_TEST_TARGETS)) \
        |& $(TEE) $(COMPONENT_TEST_OUTPUT)
    $(COMPONENT_POST_TEST_ACTION)
    $(COMPONENT_TEST_CREATE_TRANSFORMS)
    $(COMPONENT_TEST_PERFORM_TRANSFORM)
    $(COMPONENT_TEST_CLEANUP)
    $(TOUCH) $@
 
ifeq ($(strip $(SINGLE_PYTHON_VERSION)),no)
# Temporarily create symlinks for renamed binaries
COMPONENT_PRE_TEST_ACTION += \
    for f in $(PROTOUSRBINDIR)/*-$(PYTHON_VERSION) ; do \
        [ -f $$f ] || continue ; \
        [ -L $${f%%-$(PYTHON_VERSION)} ] && $(RM) $${f%%-$(PYTHON_VERSION)} ; \
        [ -e $${f%%-$(PYTHON_VERSION)} ] && continue ; \
        $(SYMLINK) $$(basename $$f) $${f%%-$(PYTHON_VERSION)} ; \
    done ;
 
# Cleanup of temporary symlinks
COMPONENT_POST_TEST_ACTION += \
    for f in $(PROTOUSRBINDIR)/*-$(PYTHON_VERSION) ; do \
        [ -f $$f ] || continue ; \
        [ ! -L $${f%%-$(PYTHON_VERSION)} ] || $(RM) $${f%%-$(PYTHON_VERSION)} ; \
    done ;
endif
 
 
ifeq ($(strip $(SINGLE_PYTHON_VERSION)),no)
# We need to add -$(PYV) to package fmri
GENERATE_EXTRA_CMD += | \
    $(GSED) -e 's/^\(set name=pkg.fmri [^@]*\)\(.*\)$$/\1-$$(PYV)\2/'
endif
 
# Add runtime dependencies from project metadata to generated manifest
GENERATE_EXTRA_DEPS += $(BUILD_DIR)/META.depend-runtime.res
GENERATE_EXTRA_CMD += | \
    $(CAT) - <( \
        echo "" ; \
        echo "\# python modules are unusable without python runtime binary" ; \
        echo "depend type=require fmri=__TBD pkg.debug.depend.file=python\$$(PYVER) \\" ; \
        echo "    pkg.debug.depend.path=usr/bin" ; \
        echo "" ; \
        echo "\# Automatically generated dependencies based on distribution metadata" ; \
        $(CAT) $(BUILD_DIR)/META.depend-runtime.res \
    )
 
# Add runtime dependencies from project metadata to REQUIRED_PACKAGES
REQUIRED_PACKAGES_RESOLVED += $(BUILD_DIR)/META.depend-runtime.res
 
 
# Generate raw lists of runtime dependencies per Python version
COMPONENT_POST_INSTALL_ACTION += \
    PYTHONPATH=$(PROTO_DIR)/$(PYTHON_DIR)/site-packages:$(PROTO_DIR)/$(PYTHON_LIB) \
        $(PYTHON) $(WS_TOOLS)/python-requires $(COMPONENT_NAME) >> $(@D)/.depend-runtime ;
 
# Convert raw per version lists of runtime dependencies to single resolved
# runtime dependency list.  The dependency on META.depend-test.required here is
# purely to get the file created as a side effect of this target.
$(BUILD_DIR)/META.depend-runtime.res:    $(INSTALL_$(MK_BITS)) $(BUILD_DIR)/META.depend-test.required
    $(CAT) $(INSTALL_$(MK_BITS):%.installed=%.depend-runtime) | $(SORT) -u \
        | $(GSED) -e 's/.*/depend type=require fmri=pkg:\/library\/python\/&-$$(PYV)/' > $@
 
# Generate raw lists of test dependencies per Python version
COMPONENT_POST_INSTALL_ACTION += \
    cd $(@D)$(COMPONENT_SUBDIR:%=/%) ; \
    ( for f in $(TEST_REQUIREMENTS) ; do \
        $(CAT) $$f | $(DOS2UNIX) -ascii ; \
    done ; \
    for e in $(TEST_REQUIREMENTS_EXTRAS) ; do \
        PYTHONPATH=$(PROTO_DIR)/$(PYTHON_DIR)/site-packages:$(PROTO_DIR)/$(PYTHON_LIB) \
            $(PYTHON) $(WS_TOOLS)/python-requires $(COMPONENT_NAME) $$e ; \
    done ) | $(WS_TOOLS)/python-resolve-deps \
        PYTHONPATH=$(PROTO_DIR)/$(PYTHON_DIR)/site-packages:$(PROTO_DIR)/$(PYTHON_LIB) \
        $(PYTHON) $(WS_TOOLS)/python-requires $(COMPONENT_NAME) \
    | $(PYTHON) $(WS_TOOLS)/python-requires - >> $(@D)/.depend-test ;
 
# Convert raw per version lists of test dependencies to single list of
# TEST_REQUIRED_PACKAGES entries.  Some Python projects lists their own project
# as a test dependency so filter this out here too.
$(BUILD_DIR)/META.depend-test.required:    $(INSTALL_$(MK_BITS))
    $(CAT) $(INSTALL_$(MK_BITS):%.installed=%.depend-test) | $(SORT) -u \
        | $(GSED) -e 's/.*/TEST_REQUIRED_PACKAGES.python += library\/python\/&/' \
        | ( $(GNU_GREP) -v ' $(COMPONENT_FMRI)$$' || true ) \
        > $@
 
# Add META.depend-test.required to the generated list of REQUIRED_PACKAGES
REQUIRED_PACKAGES_TRANSFORM += -e '$$r $(BUILD_DIR)/META.depend-test.required'
 
# The python-requires script requires packaging to provide useful output but
# packaging might be unavailable during bootstrap until we reach bootstrap
# checkpoint 2.  So require it conditionally.
ifeq ($(filter $(strip $(COMPONENT_NAME)),$(PYTHON_BOOTSTRAP_CHECKPOINT_2)),)
USERLAND_REQUIRED_PACKAGES.python += library/python/packaging
endif
 
 
clean::
    $(RM) -r $(SOURCE_DIR) $(BUILD_DIR)
 
# Make it easy to construct a URL for a pypi source download.
pypi_url_multi = pypi:///$(COMPONENT_NAME_$(1))==$(COMPONENT_VERSION_$(1))
pypi_url_single = pypi:///$(COMPONENT_NAME)==$(COMPONENT_VERSION)
pypi_url = $(if $(COMPONENT_NAME_$(1)),$(pypi_url_multi),$(pypi_url_single))