mirror of
https://gitee.com/bianbu-linux/mesa3d
synced 2025-06-28 11:13:05 -04:00
Update for v2.0.4
This commit is contained in:
parent
b5fd683534
commit
7829523a9f
6332 changed files with 1725408 additions and 496992 deletions
36
CODEOWNERS
36
CODEOWNERS
|
@ -40,11 +40,11 @@ meson.build @dbaker @eric
|
||||||
##########
|
##########
|
||||||
|
|
||||||
# NIR
|
# NIR
|
||||||
/src/compiler/nir/ @jekstrand
|
/src/compiler/nir/ @gfxstrand
|
||||||
|
|
||||||
# Vulkan
|
# Vulkan
|
||||||
/src/vulkan/ @eric @jekstrand
|
/src/vulkan/ @eric @gfxstrand
|
||||||
/include/vulkan/ @eric @jekstrand
|
/include/vulkan/ @eric @gfxstrand
|
||||||
|
|
||||||
|
|
||||||
#############
|
#############
|
||||||
|
@ -79,12 +79,9 @@ meson.build @dbaker @eric
|
||||||
/src/glx/*glvnd* @kbrenneman
|
/src/glx/*glvnd* @kbrenneman
|
||||||
|
|
||||||
# Haiku
|
# Haiku
|
||||||
/include/HaikuGL/ @kallisti5
|
|
||||||
/src/egl/drivers/haiku/ @kallisti5
|
/src/egl/drivers/haiku/ @kallisti5
|
||||||
/src/gallium/frontends/hgl/ @kallisti5
|
/src/gallium/frontends/hgl/ @kallisti5
|
||||||
/src/gallium/targets/haiku-softpipe/ @kallisti5
|
|
||||||
/src/gallium/winsys/sw/hgl/ @kallisti5
|
/src/gallium/winsys/sw/hgl/ @kallisti5
|
||||||
/src/hgl/ @kallisti5
|
|
||||||
|
|
||||||
# Loader - DRI/classic
|
# Loader - DRI/classic
|
||||||
/src/loader/ @xexaxo
|
/src/loader/ @xexaxo
|
||||||
|
@ -123,16 +120,16 @@ meson.build @dbaker @eric
|
||||||
/src/gallium/drivers/freedreno/ @robclark
|
/src/gallium/drivers/freedreno/ @robclark
|
||||||
|
|
||||||
# Imagination
|
# Imagination
|
||||||
/include/drm-uapi/pvr_drm.h @CreativeCylon @frankbinns
|
/include/drm-uapi/pvr_drm.h @CreativeCylon @frankbinns @MTCoster
|
||||||
/src/imagination/ @CreativeCylon @frankbinns
|
/src/imagination/ @CreativeCylon @frankbinns @MTCoster
|
||||||
/src/imagination/rogue/ @simon-perretta-img
|
/src/imagination/rogue/ @simon-perretta-img
|
||||||
|
|
||||||
# Intel
|
# Intel
|
||||||
/include/drm-uapi/i915_drm.h @kwg @llandwerlin @jekstrand @idr
|
/include/drm-uapi/i915_drm.h @kwg @llandwerlin @gfxstrand @idr
|
||||||
/include/pci_ids/i*_pci_ids.h @kwg @llandwerlin @jekstrand @idr
|
/include/pci_ids/i*_pci_ids.h @kwg @llandwerlin @gfxstrand @idr
|
||||||
/src/intel/ @kwg @llandwerlin @jekstrand @idr
|
/src/intel/ @kwg @llandwerlin @gfxstrand @idr
|
||||||
/src/gallium/winsys/iris/ @kwg @llandwerlin @jekstrand @idr
|
/src/gallium/winsys/iris/ @kwg @llandwerlin @gfxstrand @idr
|
||||||
/src/gallium/drivers/iris/ @kwg @llandwerlin @jekstrand @idr
|
/src/gallium/drivers/iris/ @kwg @llandwerlin @gfxstrand @idr
|
||||||
/src/gallium/drivers/i915/ @anholt
|
/src/gallium/drivers/i915/ @anholt
|
||||||
|
|
||||||
# Microsoft
|
# Microsoft
|
||||||
|
@ -140,9 +137,16 @@ meson.build @dbaker @eric
|
||||||
/src/gallium/drivers/d3d12/ @jenatali
|
/src/gallium/drivers/d3d12/ @jenatali
|
||||||
|
|
||||||
# Panfrost
|
# Panfrost
|
||||||
/src/panfrost/ @alyssa
|
/src/panfrost/ @bbrezillon
|
||||||
/src/panfrost/vulkan/ @bbrezillon
|
/src/panfrost/midgard @italove
|
||||||
/src/gallium/drivers/panfrost/ @alyssa
|
/src/gallium/drivers/panfrost/ @bbrezillon
|
||||||
|
|
||||||
|
# R300
|
||||||
|
/src/gallium/drivers/r300/ @ondracka @gawin
|
||||||
|
|
||||||
|
# VirGL - Video
|
||||||
|
/src/gallium/drivers/virgl/virgl_video.* @flynnjiang
|
||||||
|
/src/virtio/virtio-gpu/virgl_video_hw.h @flynnjiang
|
||||||
|
|
||||||
# VMware
|
# VMware
|
||||||
/src/gallium/drivers/svga/ @brianp @charmainel
|
/src/gallium/drivers/svga/ @brianp @charmainel
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
22.3.5
|
24.0.1
|
||||||
|
|
|
@ -34,15 +34,15 @@ MESA_VK_LIB_SUFFIX_intel_hasvk := intel_hasvk
|
||||||
MESA_VK_LIB_SUFFIX_freedreno := freedreno
|
MESA_VK_LIB_SUFFIX_freedreno := freedreno
|
||||||
MESA_VK_LIB_SUFFIX_broadcom := broadcom
|
MESA_VK_LIB_SUFFIX_broadcom := broadcom
|
||||||
MESA_VK_LIB_SUFFIX_panfrost := panfrost
|
MESA_VK_LIB_SUFFIX_panfrost := panfrost
|
||||||
MESA_VK_LIB_SUFFIX_virtio-experimental := virtio
|
MESA_VK_LIB_SUFFIX_virtio := virtio
|
||||||
MESA_VK_LIB_SUFFIX_swrast := lvp
|
MESA_VK_LIB_SUFFIX_swrast := lvp
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
include $(CLEAR_VARS)
|
||||||
|
|
||||||
LOCAL_SHARED_LIBRARIES := libc libdl libdrm libm liblog libcutils libz libc++ libnativewindow libsync libhardware
|
LOCAL_SHARED_LIBRARIES := libc libdl libdrm libm liblog libcutils libz libc++ libnativewindow libsync libhardware
|
||||||
LOCAL_STATIC_LIBRARIES := libexpat libarect libelf
|
LOCAL_STATIC_LIBRARIES := libexpat libarect libelf
|
||||||
LOCAL_HEADER_LIBRARIES := libnativebase_headers hwvulkan_headers libbacktrace_headers
|
LOCAL_HEADER_LIBRARIES := libnativebase_headers hwvulkan_headers
|
||||||
MESON_GEN_PKGCONFIGS := backtrace cutils expat hardware libdrm:$(LIBDRM_VERSION) nativewindow sync zlib:1.2.11 libelf
|
MESON_GEN_PKGCONFIGS := cutils expat hardware libdrm:$(LIBDRM_VERSION) nativewindow sync zlib:1.2.11 libelf
|
||||||
LOCAL_CFLAGS += $(BOARD_MESA3D_CFLAGS)
|
LOCAL_CFLAGS += $(BOARD_MESA3D_CFLAGS)
|
||||||
|
|
||||||
ifneq ($(filter swrast,$(BOARD_MESA3D_GALLIUM_DRIVERS) $(BOARD_MESA3D_VULKAN_DRIVERS)),)
|
ifneq ($(filter swrast,$(BOARD_MESA3D_GALLIUM_DRIVERS) $(BOARD_MESA3D_VULKAN_DRIVERS)),)
|
||||||
|
@ -61,9 +61,15 @@ LOCAL_SHARED_LIBRARIES += libdrm_intel
|
||||||
MESON_GEN_PKGCONFIGS += libdrm_intel:$(LIBDRM_VERSION)
|
MESON_GEN_PKGCONFIGS += libdrm_intel:$(LIBDRM_VERSION)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifneq ($(filter radeonsi amd,$(BOARD_MESA3D_GALLIUM_DRIVERS) $(BOARD_MESA3D_VULKAN_DRIVERS)),)
|
ifneq ($(filter radeonsi,$(BOARD_MESA3D_GALLIUM_DRIVERS)),)
|
||||||
MESON_GEN_LLVM_STUB := true
|
ifneq ($(MESON_GEN_LLVM_STUB),)
|
||||||
LOCAL_CFLAGS += -DFORCE_BUILD_AMDGPU # instructs LLVM to declare LLVMInitializeAMDGPU* functions
|
LOCAL_CFLAGS += -DFORCE_BUILD_AMDGPU # instructs LLVM to declare LLVMInitializeAMDGPU* functions
|
||||||
|
# The flag is required for the Android-x86 LLVM port that follows the AOSP LLVM porting rules
|
||||||
|
# https://osdn.net/projects/android-x86/scm/git/external-llvm-project
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifneq ($(filter radeonsi amd,$(BOARD_MESA3D_GALLIUM_DRIVERS) $(BOARD_MESA3D_VULKAN_DRIVERS)),)
|
||||||
LOCAL_SHARED_LIBRARIES += libdrm_amdgpu
|
LOCAL_SHARED_LIBRARIES += libdrm_amdgpu
|
||||||
MESON_GEN_PKGCONFIGS += libdrm_amdgpu:$(LIBDRM_VERSION)
|
MESON_GEN_PKGCONFIGS += libdrm_amdgpu:$(LIBDRM_VERSION)
|
||||||
endif
|
endif
|
||||||
|
@ -158,6 +164,7 @@ include $(BUILD_PREBUILT)
|
||||||
endif
|
endif
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
ifneq ($(strip $(BOARD_MESA3D_GALLIUM_DRIVERS)),)
|
||||||
# Module 'libgallium_dri', produces '/vendor/lib{64}/dri/libgallium_dri.so'
|
# Module 'libgallium_dri', produces '/vendor/lib{64}/dri/libgallium_dri.so'
|
||||||
# This module also trigger DRI symlinks creation process
|
# This module also trigger DRI symlinks creation process
|
||||||
$(eval $(call mesa3d-lib,libgallium_dri,.so.0,dri,MESA3D_GALLIUM_DRI_BIN))
|
$(eval $(call mesa3d-lib,libgallium_dri,.so.0,dri,MESA3D_GALLIUM_DRI_BIN))
|
||||||
|
@ -170,6 +177,7 @@ $(eval $(call mesa3d-lib,libEGL_mesa,.so.1,egl,MESA3D_LIBEGL_BIN))
|
||||||
$(eval $(call mesa3d-lib,libGLESv1_CM_mesa,.so.1,egl,MESA3D_LIBGLESV1_BIN))
|
$(eval $(call mesa3d-lib,libGLESv1_CM_mesa,.so.1,egl,MESA3D_LIBGLESV1_BIN))
|
||||||
# Module 'libGLESv2_mesa', produces '/vendor/lib{64}/egl/libGLESv2_mesa.so'
|
# Module 'libGLESv2_mesa', produces '/vendor/lib{64}/egl/libGLESv2_mesa.so'
|
||||||
$(eval $(call mesa3d-lib,libGLESv2_mesa,.so.2,egl,MESA3D_LIBGLESV2_BIN))
|
$(eval $(call mesa3d-lib,libGLESv2_mesa,.so.2,egl,MESA3D_LIBGLESV2_BIN))
|
||||||
|
endif
|
||||||
|
|
||||||
# Modules 'vulkan.{driver_name}', produces '/vendor/lib{64}/hw/vulkan.{driver_name}.so' HAL
|
# Modules 'vulkan.{driver_name}', produces '/vendor/lib{64}/hw/vulkan.{driver_name}.so' HAL
|
||||||
$(foreach driver,$(BOARD_MESA3D_VULKAN_DRIVERS), \
|
$(foreach driver,$(BOARD_MESA3D_VULKAN_DRIVERS), \
|
||||||
|
|
|
@ -88,9 +88,12 @@ MESON_GEN_NINJA := \
|
||||||
-Dgallium-drivers=$(subst $(space),$(comma),$(BOARD_MESA3D_GALLIUM_DRIVERS)) \
|
-Dgallium-drivers=$(subst $(space),$(comma),$(BOARD_MESA3D_GALLIUM_DRIVERS)) \
|
||||||
-Dvulkan-drivers=$(subst $(space),$(comma),$(subst radeon,amd,$(BOARD_MESA3D_VULKAN_DRIVERS))) \
|
-Dvulkan-drivers=$(subst $(space),$(comma),$(subst radeon,amd,$(BOARD_MESA3D_VULKAN_DRIVERS))) \
|
||||||
-Dgbm=enabled \
|
-Dgbm=enabled \
|
||||||
-Degl=enabled \
|
-Degl=$(if $(BOARD_MESA3D_GALLIUM_DRIVERS),enabled,disabled) \
|
||||||
|
-Dllvm=$(if $(MESON_GEN_LLVM_STUB),enabled,disabled) \
|
||||||
-Dcpp_rtti=false \
|
-Dcpp_rtti=false \
|
||||||
-Dlmsensors=disabled \
|
-Dlmsensors=disabled \
|
||||||
|
-Dandroid-libbacktrace=disabled \
|
||||||
|
$(BOARD_MESA3D_MESON_ARGS) \
|
||||||
|
|
||||||
MESON_BUILD := PATH=/usr/bin:/bin:/sbin:$$PATH ninja -C $(MESON_OUT_DIR)/build
|
MESON_BUILD := PATH=/usr/bin:/bin:/sbin:$$PATH ninja -C $(MESON_OUT_DIR)/build
|
||||||
|
|
||||||
|
@ -202,7 +205,9 @@ define m-c-flags
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define filter-c-flags
|
define filter-c-flags
|
||||||
$(filter-out -std=gnu++17 -std=gnu++14 -std=gnu99 -fno-rtti, \
|
$(filter-out -std=gnu++17 -std=gnu++14 -std=gnu99 -fno-rtti \
|
||||||
|
-enable-trivial-auto-var-init-zero-knowing-it-will-be-removed-from-clang \
|
||||||
|
-ftrivial-auto-var-init=zero,
|
||||||
$(patsubst -W%,, $1))
|
$(patsubst -W%,, $1))
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
@ -288,7 +293,7 @@ $(MESON_OUT_DIR)/install/.install.timestamp: $(MESON_OUT_DIR)/.build.timestamp
|
||||||
rm -rf $(dir $@)
|
rm -rf $(dir $@)
|
||||||
mkdir -p $(dir $@)
|
mkdir -p $(dir $@)
|
||||||
DESTDIR=$(call relative-to-absolute,$(dir $@)) $(MESON_BUILD) install
|
DESTDIR=$(call relative-to-absolute,$(dir $@)) $(MESON_BUILD) install
|
||||||
$(MESON_COPY_LIBGALLIUM)
|
$(if $(BOARD_MESA3D_GALLIUM_DRIVERS),$(MESON_COPY_LIBGALLIUM))
|
||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
$($(M_TARGET_PREFIX)MESA3D_LIBGBM_BIN) $(MESA3D_GLES_BINS): $(MESON_OUT_DIR)/install/.install.timestamp
|
$($(M_TARGET_PREFIX)MESA3D_LIBGBM_BIN) $(MESA3D_GLES_BINS): $(MESON_OUT_DIR)/install/.install.timestamp
|
||||||
|
|
2
bin/ci/.gitignore
vendored
Normal file
2
bin/ci/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
schema.graphql
|
||||||
|
gitlab_gql.py.cache*
|
413
bin/ci/ci_run_n_monitor.py
Executable file
413
bin/ci/ci_run_n_monitor.py
Executable file
|
@ -0,0 +1,413 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright © 2020 - 2022 Collabora Ltd.
|
||||||
|
# Authors:
|
||||||
|
# Tomeu Vizoso <tomeu.vizoso@collabora.com>
|
||||||
|
# David Heidelberg <david.heidelberg@collabora.com>
|
||||||
|
#
|
||||||
|
# For the dependencies, see the requirements.txt
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Helper script to restrict running only required CI jobs
|
||||||
|
and show the job(s) logs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from functools import partial
|
||||||
|
from itertools import chain
|
||||||
|
from subprocess import check_output
|
||||||
|
from typing import TYPE_CHECKING, Iterable, Literal, Optional
|
||||||
|
|
||||||
|
import gitlab
|
||||||
|
from colorama import Fore, Style
|
||||||
|
from gitlab_common import (
|
||||||
|
get_gitlab_project,
|
||||||
|
read_token,
|
||||||
|
wait_for_pipeline,
|
||||||
|
pretty_duration,
|
||||||
|
)
|
||||||
|
from gitlab_gql import GitlabGQL, create_job_needs_dag, filter_dag, print_dag
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from gitlab_gql import Dag
|
||||||
|
|
||||||
|
GITLAB_URL = "https://gitlab.freedesktop.org"
|
||||||
|
|
||||||
|
REFRESH_WAIT_LOG = 10
|
||||||
|
REFRESH_WAIT_JOBS = 6
|
||||||
|
|
||||||
|
URL_START = "\033]8;;"
|
||||||
|
URL_END = "\033]8;;\a"
|
||||||
|
|
||||||
|
STATUS_COLORS = {
|
||||||
|
"created": "",
|
||||||
|
"running": Fore.BLUE,
|
||||||
|
"success": Fore.GREEN,
|
||||||
|
"failed": Fore.RED,
|
||||||
|
"canceled": Fore.MAGENTA,
|
||||||
|
"manual": "",
|
||||||
|
"pending": "",
|
||||||
|
"skipped": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
COMPLETED_STATUSES = ["success", "failed"]
|
||||||
|
|
||||||
|
|
||||||
|
def print_job_status(job, new_status=False) -> None:
|
||||||
|
"""It prints a nice, colored job status with a link to the job."""
|
||||||
|
if job.status == "canceled":
|
||||||
|
return
|
||||||
|
|
||||||
|
if job.duration:
|
||||||
|
duration = job.duration
|
||||||
|
elif job.started_at:
|
||||||
|
duration = time.perf_counter() - time.mktime(job.started_at.timetuple())
|
||||||
|
|
||||||
|
print(
|
||||||
|
STATUS_COLORS[job.status]
|
||||||
|
+ "🞋 job "
|
||||||
|
+ URL_START
|
||||||
|
+ f"{job.web_url}\a{job.name}"
|
||||||
|
+ URL_END
|
||||||
|
+ (f" has new status: {job.status}" if new_status else f" :: {job.status}")
|
||||||
|
+ (f" ({pretty_duration(duration)})" if job.started_at else "")
|
||||||
|
+ Style.RESET_ALL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_wait(sec: int) -> None:
|
||||||
|
"""shows progressbar in dots"""
|
||||||
|
for val in range(sec, 0, -1):
|
||||||
|
print(f"⏲ {val} seconds", end="\r")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_pipeline(
|
||||||
|
project,
|
||||||
|
pipeline,
|
||||||
|
target_jobs_regex: re.Pattern,
|
||||||
|
dependencies,
|
||||||
|
force_manual: bool,
|
||||||
|
stress: int,
|
||||||
|
) -> tuple[Optional[int], Optional[int]]:
|
||||||
|
"""Monitors pipeline and delegate canceling jobs"""
|
||||||
|
statuses: dict[str, str] = defaultdict(str)
|
||||||
|
target_statuses: dict[str, str] = defaultdict(str)
|
||||||
|
stress_status_counter = defaultdict(lambda: defaultdict(int))
|
||||||
|
target_id = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
deps_failed = []
|
||||||
|
to_cancel = []
|
||||||
|
for job in pipeline.jobs.list(all=True, sort="desc"):
|
||||||
|
# target jobs
|
||||||
|
if target_jobs_regex.fullmatch(job.name):
|
||||||
|
target_id = job.id
|
||||||
|
|
||||||
|
if stress and job.status in ["success", "failed"]:
|
||||||
|
if (
|
||||||
|
stress < 0
|
||||||
|
or sum(stress_status_counter[job.name].values()) < stress
|
||||||
|
):
|
||||||
|
enable_job(project, job, "retry", force_manual)
|
||||||
|
stress_status_counter[job.name][job.status] += 1
|
||||||
|
else:
|
||||||
|
enable_job(project, job, "target", force_manual)
|
||||||
|
|
||||||
|
print_job_status(job, job.status not in target_statuses[job.name])
|
||||||
|
target_statuses[job.name] = job.status
|
||||||
|
continue
|
||||||
|
|
||||||
|
# all jobs
|
||||||
|
if job.status != statuses[job.name]:
|
||||||
|
print_job_status(job, True)
|
||||||
|
statuses[job.name] = job.status
|
||||||
|
|
||||||
|
# run dependencies and cancel the rest
|
||||||
|
if job.name in dependencies:
|
||||||
|
enable_job(project, job, "dep", True)
|
||||||
|
if job.status == "failed":
|
||||||
|
deps_failed.append(job.name)
|
||||||
|
else:
|
||||||
|
to_cancel.append(job)
|
||||||
|
|
||||||
|
cancel_jobs(project, to_cancel)
|
||||||
|
|
||||||
|
if stress:
|
||||||
|
enough = True
|
||||||
|
for job_name, status in stress_status_counter.items():
|
||||||
|
print(
|
||||||
|
f"{job_name}\tsucc: {status['success']}; "
|
||||||
|
f"fail: {status['failed']}; "
|
||||||
|
f"total: {sum(status.values())} of {stress}",
|
||||||
|
flush=False,
|
||||||
|
)
|
||||||
|
if stress < 0 or sum(status.values()) < stress:
|
||||||
|
enough = False
|
||||||
|
|
||||||
|
if not enough:
|
||||||
|
pretty_wait(REFRESH_WAIT_JOBS)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("---------------------------------", flush=False)
|
||||||
|
|
||||||
|
if len(target_statuses) == 1 and {"running"}.intersection(
|
||||||
|
target_statuses.values()
|
||||||
|
):
|
||||||
|
return target_id, None
|
||||||
|
|
||||||
|
if (
|
||||||
|
{"failed"}.intersection(target_statuses.values())
|
||||||
|
and not set(["running", "pending"]).intersection(target_statuses.values())
|
||||||
|
):
|
||||||
|
return None, 1
|
||||||
|
|
||||||
|
if (
|
||||||
|
{"skipped"}.intersection(target_statuses.values())
|
||||||
|
and not {"running", "pending"}.intersection(target_statuses.values())
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
Fore.RED,
|
||||||
|
"Target in skipped state, aborting. Failed dependencies:",
|
||||||
|
deps_failed,
|
||||||
|
Fore.RESET,
|
||||||
|
)
|
||||||
|
return None, 1
|
||||||
|
|
||||||
|
if {"success", "manual"}.issuperset(target_statuses.values()):
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
pretty_wait(REFRESH_WAIT_JOBS)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_job(
|
||||||
|
project, job, action_type: Literal["target", "dep", "retry"], force_manual: bool
|
||||||
|
) -> None:
|
||||||
|
"""enable job"""
|
||||||
|
if (
|
||||||
|
(job.status in ["success", "failed"] and action_type != "retry")
|
||||||
|
or (job.status == "manual" and not force_manual)
|
||||||
|
or job.status in ["skipped", "running", "created", "pending"]
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
pjob = project.jobs.get(job.id, lazy=True)
|
||||||
|
|
||||||
|
if job.status in ["success", "failed", "canceled"]:
|
||||||
|
pjob.retry()
|
||||||
|
else:
|
||||||
|
pjob.play()
|
||||||
|
|
||||||
|
if action_type == "target":
|
||||||
|
jtype = "🞋 "
|
||||||
|
elif action_type == "retry":
|
||||||
|
jtype = "↻"
|
||||||
|
else:
|
||||||
|
jtype = "(dependency)"
|
||||||
|
|
||||||
|
print(Fore.MAGENTA + f"{jtype} job {job.name} manually enabled" + Style.RESET_ALL)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_job(project, job) -> None:
|
||||||
|
"""Cancel GitLab job"""
|
||||||
|
if job.status in [
|
||||||
|
"canceled",
|
||||||
|
"success",
|
||||||
|
"failed",
|
||||||
|
"skipped",
|
||||||
|
]:
|
||||||
|
return
|
||||||
|
pjob = project.jobs.get(job.id, lazy=True)
|
||||||
|
pjob.cancel()
|
||||||
|
print(f"♲ {job.name}", end=" ")
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_jobs(project, to_cancel) -> None:
|
||||||
|
"""Cancel unwanted GitLab jobs"""
|
||||||
|
if not to_cancel:
|
||||||
|
return
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=6) as exe:
|
||||||
|
part = partial(cancel_job, project)
|
||||||
|
exe.map(part, to_cancel)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def print_log(project, job_id) -> None:
|
||||||
|
"""Print job log into output"""
|
||||||
|
printed_lines = 0
|
||||||
|
while True:
|
||||||
|
job = project.jobs.get(job_id)
|
||||||
|
|
||||||
|
# GitLab's REST API doesn't offer pagination for logs, so we have to refetch it all
|
||||||
|
lines = job.trace().decode("raw_unicode_escape").splitlines()
|
||||||
|
for line in lines[printed_lines:]:
|
||||||
|
print(line)
|
||||||
|
printed_lines = len(lines)
|
||||||
|
|
||||||
|
if job.status in COMPLETED_STATUSES:
|
||||||
|
print(Fore.GREEN + f"Job finished: {job.web_url}" + Style.RESET_ALL)
|
||||||
|
return
|
||||||
|
pretty_wait(REFRESH_WAIT_LOG)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> None:
|
||||||
|
"""Parse args"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Tool to trigger a subset of container jobs "
|
||||||
|
+ "and monitor the progress of a test job",
|
||||||
|
epilog="Example: mesa-monitor.py --rev $(git rev-parse HEAD) "
|
||||||
|
+ '--target ".*traces" ',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--target",
|
||||||
|
metavar="target-job",
|
||||||
|
help="Target job regex. For multiple targets, separate with pipe | character",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--token",
|
||||||
|
metavar="token",
|
||||||
|
help="force GitLab token, otherwise it's read from ~/.config/gitlab-token",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force-manual", action="store_true", help="Force jobs marked as manual"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stress",
|
||||||
|
default=0,
|
||||||
|
type=int,
|
||||||
|
help="Stresstest job(s). Number or repetitions or -1 for infinite.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--project",
|
||||||
|
default="mesa",
|
||||||
|
help="GitLab project in the format <user>/<project> or just <project>",
|
||||||
|
)
|
||||||
|
|
||||||
|
mutex_group1 = parser.add_mutually_exclusive_group()
|
||||||
|
mutex_group1.add_argument(
|
||||||
|
"--rev", default="HEAD", metavar="revision", help="repository git revision (default: HEAD)"
|
||||||
|
)
|
||||||
|
mutex_group1.add_argument(
|
||||||
|
"--pipeline-url",
|
||||||
|
help="URL of the pipeline to use, instead of auto-detecting it.",
|
||||||
|
)
|
||||||
|
mutex_group1.add_argument(
|
||||||
|
"--mr",
|
||||||
|
type=int,
|
||||||
|
help="ID of a merge request; the latest pipeline in that MR will be used.",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# argparse doesn't support groups inside add_mutually_exclusive_group(),
|
||||||
|
# which means we can't just put `--project` and `--rev` in a group together,
|
||||||
|
# we have to do this by heand instead.
|
||||||
|
if args.pipeline_url and args.project != parser.get_default("project"):
|
||||||
|
# weird phrasing but it's the error add_mutually_exclusive_group() gives
|
||||||
|
parser.error("argument --project: not allowed with argument --pipeline-url")
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def print_detected_jobs(
|
||||||
|
target_dep_dag: "Dag", dependency_jobs: Iterable[str], target_jobs: Iterable[str]
|
||||||
|
) -> None:
|
||||||
|
def print_job_set(color: str, kind: str, job_set: Iterable[str]):
|
||||||
|
print(
|
||||||
|
color + f"Running {len(job_set)} {kind} jobs: ",
|
||||||
|
"\n",
|
||||||
|
", ".join(sorted(job_set)),
|
||||||
|
Fore.RESET,
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(Fore.YELLOW + "Detected target job and its dependencies:", "\n")
|
||||||
|
print_dag(target_dep_dag)
|
||||||
|
print_job_set(Fore.MAGENTA, "dependency", dependency_jobs)
|
||||||
|
print_job_set(Fore.BLUE, "target", target_jobs)
|
||||||
|
|
||||||
|
|
||||||
|
def find_dependencies(target_jobs_regex: re.Pattern, project_path: str, iid: int) -> set[str]:
|
||||||
|
gql_instance = GitlabGQL()
|
||||||
|
dag = create_job_needs_dag(
|
||||||
|
gql_instance, {"projectPath": project_path.path_with_namespace, "iid": iid}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_dep_dag = filter_dag(dag, target_jobs_regex)
|
||||||
|
if not target_dep_dag:
|
||||||
|
print(Fore.RED + "The job(s) were not found in the pipeline." + Fore.RESET)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
dependency_jobs = set(chain.from_iterable(d["needs"] for d in target_dep_dag.values()))
|
||||||
|
target_jobs = set(target_dep_dag.keys())
|
||||||
|
print_detected_jobs(target_dep_dag, dependency_jobs, target_jobs)
|
||||||
|
return target_jobs.union(dependency_jobs)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
t_start = time.perf_counter()
|
||||||
|
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
token = read_token(args.token)
|
||||||
|
|
||||||
|
gl = gitlab.Gitlab(url=GITLAB_URL,
|
||||||
|
private_token=token,
|
||||||
|
retry_transient_errors=True)
|
||||||
|
|
||||||
|
REV: str = args.rev
|
||||||
|
|
||||||
|
if args.pipeline_url:
|
||||||
|
assert args.pipeline_url.startswith(GITLAB_URL)
|
||||||
|
url_path = args.pipeline_url[len(GITLAB_URL):]
|
||||||
|
url_path_components = url_path.split("/")
|
||||||
|
project_name = "/".join(url_path_components[1:3])
|
||||||
|
assert url_path_components[3] == "-"
|
||||||
|
assert url_path_components[4] == "pipelines"
|
||||||
|
pipeline_id = int(url_path_components[5])
|
||||||
|
cur_project = gl.projects.get(project_name)
|
||||||
|
pipe = cur_project.pipelines.get(pipeline_id)
|
||||||
|
REV = pipe.sha
|
||||||
|
else:
|
||||||
|
mesa_project = gl.projects.get("mesa/mesa")
|
||||||
|
projects = [mesa_project]
|
||||||
|
if args.mr:
|
||||||
|
REV = mesa_project.mergerequests.get(args.mr).sha
|
||||||
|
else:
|
||||||
|
REV = check_output(['git', 'rev-parse', REV]).decode('ascii').strip()
|
||||||
|
projects.append(get_gitlab_project(gl, args.project))
|
||||||
|
(pipe, cur_project) = wait_for_pipeline(projects, REV)
|
||||||
|
|
||||||
|
print(f"Revision: {REV}")
|
||||||
|
print(f"Pipeline: {pipe.web_url}")
|
||||||
|
|
||||||
|
target_jobs_regex = re.compile(args.target.strip())
|
||||||
|
|
||||||
|
deps = set()
|
||||||
|
if args.target:
|
||||||
|
print("🞋 job: " + Fore.BLUE + args.target + Style.RESET_ALL)
|
||||||
|
deps = find_dependencies(
|
||||||
|
target_jobs_regex=target_jobs_regex, iid=pipe.iid, project_path=cur_project
|
||||||
|
)
|
||||||
|
target_job_id, ret = monitor_pipeline(
|
||||||
|
cur_project, pipe, target_jobs_regex, deps, args.force_manual, args.stress
|
||||||
|
)
|
||||||
|
|
||||||
|
if target_job_id:
|
||||||
|
print_log(cur_project, target_job_id)
|
||||||
|
|
||||||
|
t_end = time.perf_counter()
|
||||||
|
spend_minutes = (t_end - t_start) / 60
|
||||||
|
print(f"⏲ Duration of script execution: {spend_minutes:0.1f} minutes")
|
||||||
|
|
||||||
|
sys.exit(ret)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
10
bin/ci/ci_run_n_monitor.sh
Executable file
10
bin/ci/ci_run_n_monitor.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
this_dir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")
|
||||||
|
readonly this_dir
|
||||||
|
|
||||||
|
exec \
|
||||||
|
"$this_dir/../python-venv.sh" \
|
||||||
|
"$this_dir/requirements.txt" \
|
||||||
|
"$this_dir/ci_run_n_monitor.py" "$@"
|
334
bin/ci/custom_logger.py
Normal file
334
bin/ci/custom_logger.py
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from structured_logger import StructuredLogger
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLogger:
|
||||||
|
def __init__(self, log_file):
|
||||||
|
self.log_file = log_file
|
||||||
|
self.logger = StructuredLogger(file_name=self.log_file)
|
||||||
|
|
||||||
|
def get_last_dut_job(self):
|
||||||
|
"""
|
||||||
|
Gets the details of the most recent DUT job.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Details of the most recent DUT job.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no DUT jobs are found in the logger's data.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
job = self.logger.data["dut_jobs"][-1]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
"No DUT jobs found. Please create a job via create_dut_job call."
|
||||||
|
)
|
||||||
|
|
||||||
|
return job
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Updates the log file with provided key-value pairs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Key-value pairs to be updated.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
self.logger.data[key] = value
|
||||||
|
|
||||||
|
def create_dut_job(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a new DUT job with provided key-value pairs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Key-value pairs for the new DUT job.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
if "dut_jobs" not in self.logger.data:
|
||||||
|
self.logger.data["dut_jobs"] = []
|
||||||
|
new_job = {
|
||||||
|
"status": "",
|
||||||
|
"submitter_start_time": datetime.now().isoformat(),
|
||||||
|
"dut_submit_time": "",
|
||||||
|
"dut_start_time": "",
|
||||||
|
"dut_end_time": "",
|
||||||
|
"dut_name": "",
|
||||||
|
"dut_state": "pending",
|
||||||
|
"dut_job_phases": [],
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
self.logger.data["dut_jobs"].append(new_job)
|
||||||
|
|
||||||
|
def update_dut_job(self, key, value):
|
||||||
|
"""
|
||||||
|
Updates the last DUT job with a key-value pair.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key : The key to be updated.
|
||||||
|
value: The value to be assigned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
job = self.get_last_dut_job()
|
||||||
|
job[key] = value
|
||||||
|
|
||||||
|
def update_status_fail(self, reason=""):
|
||||||
|
"""
|
||||||
|
Sets the status of the last DUT job to 'fail' and logs the failure reason.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reason (str, optional): The reason for the failure. Defaults to "".
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
job = self.get_last_dut_job()
|
||||||
|
job["status"] = "fail"
|
||||||
|
job["dut_job_fail_reason"] = reason
|
||||||
|
|
||||||
|
def create_job_phase(self, phase_name):
|
||||||
|
"""
|
||||||
|
Creates a new job phase for the last DUT job.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phase_name : The name of the new job phase.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
job = self.get_last_dut_job()
|
||||||
|
if job["dut_job_phases"] and job["dut_job_phases"][-1]["end_time"] == "":
|
||||||
|
# If the last phase exists and its end time is empty, set the end time
|
||||||
|
job["dut_job_phases"][-1]["end_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# Create a new phase
|
||||||
|
phase_data = {
|
||||||
|
"name": phase_name,
|
||||||
|
"start_time": datetime.now().isoformat(),
|
||||||
|
"end_time": "",
|
||||||
|
}
|
||||||
|
job["dut_job_phases"].append(phase_data)
|
||||||
|
|
||||||
|
def check_dut_timings(self, job):
|
||||||
|
"""
|
||||||
|
Check the timing sequence of a job to ensure logical consistency.
|
||||||
|
|
||||||
|
The function verifies that the job's submission time is not earlier than its start time and that
|
||||||
|
the job's end time is not earlier than its start time. If either of these conditions is found to be true,
|
||||||
|
an error is logged for each instance of inconsistency.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job (dict): A dictionary containing timing information of a job. Expected keys are 'dut_start_time',
|
||||||
|
'dut_submit_time', and 'dut_end_time'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: This function does not return a value; it logs errors if timing inconsistencies are detected.
|
||||||
|
|
||||||
|
The function checks the following:
|
||||||
|
- If 'dut_start_time' and 'dut_submit_time' are both present and correctly sequenced.
|
||||||
|
- If 'dut_start_time' and 'dut_end_time' are both present and correctly sequenced.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if the start time and submit time exist
|
||||||
|
if job.get("dut_start_time") and job.get("dut_submit_time"):
|
||||||
|
# If they exist, check if the submission time is before the start time
|
||||||
|
if job["dut_start_time"] < job["dut_submit_time"]:
|
||||||
|
logging.error("Job submission is happening before job start.")
|
||||||
|
|
||||||
|
# Check if the start time and end time exist
|
||||||
|
if job.get("dut_start_time") and job.get("dut_end_time"):
|
||||||
|
# If they exist, check if the end time is after the start time
|
||||||
|
if job["dut_end_time"] < job["dut_start_time"]:
|
||||||
|
logging.error("Job ended before it started.")
|
||||||
|
|
||||||
|
# Method to update DUT start, submit and end time
|
||||||
|
def update_dut_time(self, value, custom_time):
|
||||||
|
"""
|
||||||
|
Updates DUT start, submit, and end times.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value : Specifies which DUT time to update. Options: 'start', 'submit', 'end'.
|
||||||
|
custom_time : Custom time to set. If None, use current time.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If an invalid argument is provided for value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
job = self.get_last_dut_job()
|
||||||
|
timestamp = custom_time if custom_time else datetime.now().isoformat()
|
||||||
|
if value == "start":
|
||||||
|
job["dut_start_time"] = timestamp
|
||||||
|
job["dut_state"] = "running"
|
||||||
|
elif value == "submit":
|
||||||
|
job["dut_submit_time"] = timestamp
|
||||||
|
job["dut_state"] = "submitted"
|
||||||
|
elif value == "end":
|
||||||
|
job["dut_end_time"] = timestamp
|
||||||
|
job["dut_state"] = "finished"
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Error: Invalid argument provided for --update-dut-time. Use 'start', 'submit', 'end'."
|
||||||
|
)
|
||||||
|
# check the sanity of the partial structured log
|
||||||
|
self.check_dut_timings(job)
|
||||||
|
|
||||||
|
def close_dut_job(self):
|
||||||
|
"""
|
||||||
|
Closes the most recent DUT (Device Under Test) job in the logger's data.
|
||||||
|
|
||||||
|
The method performs the following operations:
|
||||||
|
1. Validates if there are any DUT jobs in the logger's data.
|
||||||
|
2. If the last phase of the most recent DUT job has an empty end time, it sets the end time to the current time.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no DUT jobs are found in the logger's data.
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
job = self.get_last_dut_job()
|
||||||
|
# Check if the last phase exists and its end time is empty, then set the end time
|
||||||
|
if job["dut_job_phases"] and job["dut_job_phases"][-1]["end_time"] == "":
|
||||||
|
job["dut_job_phases"][-1]["end_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Closes the most recent DUT (Device Under Test) job in the logger's data.
|
||||||
|
|
||||||
|
The method performs the following operations:
|
||||||
|
1. Determines the combined status of all DUT jobs.
|
||||||
|
2. Sets the submitter's end time to the current time.
|
||||||
|
3. Updates the DUT attempt counter to reflect the total number of DUT jobs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with self.logger.edit_context():
|
||||||
|
job_status = []
|
||||||
|
for job in self.logger.data["dut_jobs"]:
|
||||||
|
if "status" in job:
|
||||||
|
job_status.append(job["status"])
|
||||||
|
|
||||||
|
if not job_status:
|
||||||
|
job_combined_status = "null"
|
||||||
|
else:
|
||||||
|
# Get job_combined_status
|
||||||
|
if "pass" in job_status:
|
||||||
|
job_combined_status = "pass"
|
||||||
|
else:
|
||||||
|
job_combined_status = "fail"
|
||||||
|
|
||||||
|
self.logger.data["job_combined_status"] = job_combined_status
|
||||||
|
self.logger.data["dut_attempt_counter"] = len(self.logger.data["dut_jobs"])
|
||||||
|
job["submitter_end_time"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def process_args(args):
|
||||||
|
# Function to process key-value pairs and call corresponding logger methods
|
||||||
|
def process_key_value_pairs(args_list, action_func):
|
||||||
|
if not args_list:
|
||||||
|
raise ValueError(
|
||||||
|
f"No key-value pairs provided for {action_func.__name__.replace('_', '-')}"
|
||||||
|
)
|
||||||
|
if len(args_list) % 2 != 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Incomplete key-value pairs for {action_func.__name__.replace('_', '-')}"
|
||||||
|
)
|
||||||
|
kwargs = dict(zip(args_list[::2], args_list[1::2]))
|
||||||
|
action_func(**kwargs)
|
||||||
|
|
||||||
|
# Create a CustomLogger object with the specified log file path
|
||||||
|
custom_logger = CustomLogger(Path(args.log_file))
|
||||||
|
|
||||||
|
if args.update:
|
||||||
|
process_key_value_pairs(args.update, custom_logger.update)
|
||||||
|
|
||||||
|
if args.create_dut_job:
|
||||||
|
process_key_value_pairs(args.create_dut_job, custom_logger.create_dut_job)
|
||||||
|
|
||||||
|
if args.update_dut_job:
|
||||||
|
key, value = args.update_dut_job
|
||||||
|
custom_logger.update_dut_job(key, value)
|
||||||
|
|
||||||
|
if args.create_job_phase:
|
||||||
|
custom_logger.create_job_phase(args.create_job_phase)
|
||||||
|
|
||||||
|
if args.update_status_fail:
|
||||||
|
custom_logger.update_status_fail(args.update_status_fail)
|
||||||
|
|
||||||
|
if args.update_dut_time:
|
||||||
|
if len(args.update_dut_time) == 2:
|
||||||
|
action, custom_time = args.update_dut_time
|
||||||
|
elif len(args.update_dut_time) == 1:
|
||||||
|
action, custom_time = args.update_dut_time[0], None
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid number of values for --update-dut-time")
|
||||||
|
|
||||||
|
if action in ["start", "end", "submit"]:
|
||||||
|
custom_logger.update_dut_time(action, custom_time)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Error: Invalid argument provided for --update-dut-time. Use 'start', 'submit', 'end'."
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.close_dut_job:
|
||||||
|
custom_logger.close_dut_job()
|
||||||
|
|
||||||
|
if args.close:
|
||||||
|
custom_logger.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Custom Logger Command Line Tool")
|
||||||
|
parser.add_argument("log_file", help="Path to the log file")
|
||||||
|
parser.add_argument(
|
||||||
|
"--update",
|
||||||
|
nargs=argparse.ZERO_OR_MORE,
|
||||||
|
metavar=("key", "value"),
|
||||||
|
help="Update a key-value pair e.g., --update key1 value1 key2 value2)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--create-dut-job",
|
||||||
|
nargs=argparse.ZERO_OR_MORE,
|
||||||
|
metavar=("key", "value"),
|
||||||
|
help="Create a new DUT job with key-value pairs (e.g., --create-dut-job key1 value1 key2 value2)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--update-dut-job",
|
||||||
|
nargs=argparse.ZERO_OR_MORE,
|
||||||
|
metavar=("key", "value"),
|
||||||
|
help="Update a key-value pair in DUT job",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--create-job-phase",
|
||||||
|
help="Create a new job phase (e.g., --create-job-phase name)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--update-status-fail",
|
||||||
|
help="Update fail as the status and log the failure reason (e.g., --update-status-fail reason)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--update-dut-time",
|
||||||
|
nargs=argparse.ZERO_OR_MORE,
|
||||||
|
metavar=("action", "custom_time"),
|
||||||
|
help="Update DUT start and end time. Provide action ('start', 'submit', 'end') and custom_time (e.g., '2023-01-01T12:00:00')",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--close-dut-job",
|
||||||
|
action="store_true",
|
||||||
|
help="Close the dut job by updating end time of last dut job)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--close",
|
||||||
|
action="store_true",
|
||||||
|
help="Updates combined status, submitter's end time and DUT attempt counter",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
process_args(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
11
bin/ci/download_gl_schema.sh
Executable file
11
bin/ci/download_gl_schema.sh
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Helper script to download the schema GraphQL from Gitlab to enable IDEs to
|
||||||
|
# assist the developer to edit gql files
|
||||||
|
|
||||||
|
SOURCE_DIR=$(dirname "$(realpath "$0")")
|
||||||
|
|
||||||
|
(
|
||||||
|
cd $SOURCE_DIR || exit 1
|
||||||
|
gql-cli https://gitlab.freedesktop.org/api/graphql --print-schema > schema.graphql
|
||||||
|
)
|
63
bin/ci/gitlab_common.py
Normal file
63
bin/ci/gitlab_common.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright © 2020 - 2022 Collabora Ltd.
|
||||||
|
# Authors:
|
||||||
|
# Tomeu Vizoso <tomeu.vizoso@collabora.com>
|
||||||
|
# David Heidelberg <david.heidelberg@collabora.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
'''Shared functions between the scripts.'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_duration(seconds):
|
||||||
|
"""Pretty print duration"""
|
||||||
|
hours, rem = divmod(seconds, 3600)
|
||||||
|
minutes, seconds = divmod(rem, 60)
|
||||||
|
if hours:
|
||||||
|
return f"{hours:0.0f}h{minutes:0.0f}m{seconds:0.0f}s"
|
||||||
|
if minutes:
|
||||||
|
return f"{minutes:0.0f}m{seconds:0.0f}s"
|
||||||
|
return f"{seconds:0.0f}s"
|
||||||
|
|
||||||
|
|
||||||
|
def get_gitlab_project(glab, name: str):
|
||||||
|
"""Finds a specified gitlab project for given user"""
|
||||||
|
if "/" in name:
|
||||||
|
project_path = name
|
||||||
|
else:
|
||||||
|
glab.auth()
|
||||||
|
username = glab.user.username
|
||||||
|
project_path = f"{username}/{name}"
|
||||||
|
return glab.projects.get(project_path)
|
||||||
|
|
||||||
|
|
||||||
|
def read_token(token_arg: Optional[str]) -> str:
|
||||||
|
"""pick token from args or file"""
|
||||||
|
if token_arg:
|
||||||
|
return token_arg
|
||||||
|
return (
|
||||||
|
open(os.path.expanduser("~/.config/gitlab-token"), encoding="utf-8")
|
||||||
|
.readline()
|
||||||
|
.rstrip()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_pipeline(projects, sha: str, timeout=None):
|
||||||
|
"""await until pipeline appears in Gitlab"""
|
||||||
|
project_names = [project.path_with_namespace for project in projects]
|
||||||
|
print(f"⏲ for the pipeline to appear in {project_names}..", end="")
|
||||||
|
start_time = time.time()
|
||||||
|
while True:
|
||||||
|
for project in projects:
|
||||||
|
pipelines = project.pipelines.list(sha=sha)
|
||||||
|
if pipelines:
|
||||||
|
print("", flush=True)
|
||||||
|
return (pipelines[0], project)
|
||||||
|
print("", end=".", flush=True)
|
||||||
|
if timeout and time.time() - start_time > timeout:
|
||||||
|
print(" not found", flush=True)
|
||||||
|
return (None, None)
|
||||||
|
time.sleep(1)
|
548
bin/ci/gitlab_gql.py
Executable file
548
bin/ci/gitlab_gql.py
Executable file
|
@ -0,0 +1,548 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# For the dependencies, see the requirements.txt
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
|
||||||
|
from collections import OrderedDict
|
||||||
|
from copy import deepcopy
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from itertools import accumulate
|
||||||
|
from os import getenv
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import check_output
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import Any, Iterable, Optional, Pattern, TypedDict, Union
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from filecache import DAY, filecache
|
||||||
|
from gql import Client, gql
|
||||||
|
from gql.transport.requests import RequestsHTTPTransport
|
||||||
|
from graphql import DocumentNode
|
||||||
|
|
||||||
|
|
||||||
|
class DagNode(TypedDict):
|
||||||
|
needs: set[str]
|
||||||
|
stage: str
|
||||||
|
# `name` is redundant but is here for retro-compatibility
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
# see create_job_needs_dag function for more details
|
||||||
|
Dag = dict[str, DagNode]
|
||||||
|
|
||||||
|
|
||||||
|
StageSeq = OrderedDict[str, set[str]]
|
||||||
|
TOKEN_DIR = Path(getenv("XDG_CONFIG_HOME") or Path.home() / ".config")
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_from_default_dir() -> str:
|
||||||
|
token_file = TOKEN_DIR / "gitlab-token"
|
||||||
|
try:
|
||||||
|
return str(token_file.resolve())
|
||||||
|
except FileNotFoundError as ex:
|
||||||
|
print(
|
||||||
|
f"Could not find {token_file}, please provide a token file as an argument"
|
||||||
|
)
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_root_dir():
|
||||||
|
root_path = Path(__file__).parent.parent.parent.resolve()
|
||||||
|
gitlab_file = root_path / ".gitlab-ci.yml"
|
||||||
|
assert gitlab_file.exists()
|
||||||
|
|
||||||
|
return root_path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GitlabGQL:
|
||||||
|
_transport: Any = field(init=False)
|
||||||
|
client: Client = field(init=False)
|
||||||
|
url: str = "https://gitlab.freedesktop.org/api/graphql"
|
||||||
|
token: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._setup_gitlab_gql_client()
|
||||||
|
|
||||||
|
def _setup_gitlab_gql_client(self) -> None:
|
||||||
|
# Select your transport with a defined url endpoint
|
||||||
|
headers = {}
|
||||||
|
if self.token:
|
||||||
|
headers["Authorization"] = f"Bearer {self.token}"
|
||||||
|
self._transport = RequestsHTTPTransport(url=self.url, headers=headers)
|
||||||
|
|
||||||
|
# Create a GraphQL client using the defined transport
|
||||||
|
self.client = Client(transport=self._transport, fetch_schema_from_transport=True)
|
||||||
|
|
||||||
|
def query(
|
||||||
|
self,
|
||||||
|
gql_file: Union[Path, str],
|
||||||
|
params: dict[str, Any] = {},
|
||||||
|
operation_name: Optional[str] = None,
|
||||||
|
paginated_key_loc: Iterable[str] = [],
|
||||||
|
disable_cache: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
def run_uncached() -> dict[str, Any]:
|
||||||
|
if paginated_key_loc:
|
||||||
|
return self._sweep_pages(gql_file, params, operation_name, paginated_key_loc)
|
||||||
|
return self._query(gql_file, params, operation_name)
|
||||||
|
|
||||||
|
if disable_cache:
|
||||||
|
return run_uncached()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create an auxiliary variable to deliver a cached result and enable catching exceptions
|
||||||
|
# Decorate the query to be cached
|
||||||
|
if paginated_key_loc:
|
||||||
|
result = self._sweep_pages_cached(
|
||||||
|
gql_file, params, operation_name, paginated_key_loc
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = self._query_cached(gql_file, params, operation_name)
|
||||||
|
return result # type: ignore
|
||||||
|
except Exception as ex:
|
||||||
|
logging.error(f"Cached query failed with {ex}")
|
||||||
|
# print exception traceback
|
||||||
|
traceback_str = "".join(traceback.format_exception(ex))
|
||||||
|
logging.error(traceback_str)
|
||||||
|
self.invalidate_query_cache()
|
||||||
|
logging.error("Cache invalidated, retrying without cache")
|
||||||
|
finally:
|
||||||
|
return run_uncached()
|
||||||
|
|
||||||
|
def _query(
|
||||||
|
self,
|
||||||
|
gql_file: Union[Path, str],
|
||||||
|
params: dict[str, Any] = {},
|
||||||
|
operation_name: Optional[str] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
# Provide a GraphQL query
|
||||||
|
source_path: Path = Path(__file__).parent
|
||||||
|
pipeline_query_file: Path = source_path / gql_file
|
||||||
|
|
||||||
|
query: DocumentNode
|
||||||
|
with open(pipeline_query_file, "r") as f:
|
||||||
|
pipeline_query = f.read()
|
||||||
|
query = gql(pipeline_query)
|
||||||
|
|
||||||
|
# Execute the query on the transport
|
||||||
|
return self.client.execute_sync(
|
||||||
|
query, variable_values=params, operation_name=operation_name
|
||||||
|
)
|
||||||
|
|
||||||
|
@filecache(DAY)
|
||||||
|
def _sweep_pages_cached(self, *args, **kwargs):
|
||||||
|
return self._sweep_pages(*args, **kwargs)
|
||||||
|
|
||||||
|
@filecache(DAY)
|
||||||
|
def _query_cached(self, *args, **kwargs):
|
||||||
|
return self._query(*args, **kwargs)
|
||||||
|
|
||||||
|
def _sweep_pages(
|
||||||
|
self, query, params, operation_name=None, paginated_key_loc: Iterable[str] = []
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve paginated data from a GraphQL API and concatenate the results into a single
|
||||||
|
response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: represents a filepath with the GraphQL query to be executed.
|
||||||
|
params: a dictionary that contains the parameters to be passed to the query. These
|
||||||
|
parameters can be used to filter or modify the results of the query.
|
||||||
|
operation_name: The `operation_name` parameter is an optional parameter that specifies
|
||||||
|
the name of the GraphQL operation to be executed. It is used when making a GraphQL
|
||||||
|
query to specify which operation to execute if there are multiple operations defined
|
||||||
|
in the GraphQL schema. If not provided, the default operation will be executed.
|
||||||
|
paginated_key_loc (Iterable[str]): The `paginated_key_loc` parameter is an iterable of
|
||||||
|
strings that represents the location of the paginated field within the response. It
|
||||||
|
is used to extract the paginated field from the response and append it to the final
|
||||||
|
result. The node has to be a list of objects with a `pageInfo` field that contains
|
||||||
|
at least the `hasNextPage` and `endCursor` fields.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a dictionary containing the response from the query with the paginated field
|
||||||
|
concatenated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def fetch_page(cursor: str | None = None) -> dict[str, Any]:
|
||||||
|
if cursor:
|
||||||
|
params["cursor"] = cursor
|
||||||
|
logging.info(
|
||||||
|
f"Found more than 100 elements, paginating. "
|
||||||
|
f"Current cursor at {cursor}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._query(query, params, operation_name)
|
||||||
|
|
||||||
|
# Execute the initial query
|
||||||
|
response: dict[str, Any] = fetch_page()
|
||||||
|
|
||||||
|
# Initialize an empty list to store the final result
|
||||||
|
final_partial_field: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
# Loop until all pages have been retrieved
|
||||||
|
while True:
|
||||||
|
# Get the partial field to be appended to the final result
|
||||||
|
partial_field = response
|
||||||
|
for key in paginated_key_loc:
|
||||||
|
partial_field = partial_field[key]
|
||||||
|
|
||||||
|
# Append the partial field to the final result
|
||||||
|
final_partial_field += partial_field["nodes"]
|
||||||
|
|
||||||
|
# Check if there are more pages to retrieve
|
||||||
|
page_info = partial_field["pageInfo"]
|
||||||
|
if not page_info["hasNextPage"]:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Execute the query with the updated cursor parameter
|
||||||
|
response = fetch_page(page_info["endCursor"])
|
||||||
|
|
||||||
|
# Replace the "nodes" field in the original response with the final result
|
||||||
|
partial_field["nodes"] = final_partial_field
|
||||||
|
return response
|
||||||
|
|
||||||
|
def invalidate_query_cache(self) -> None:
|
||||||
|
logging.warning("Invalidating query cache")
|
||||||
|
try:
|
||||||
|
self._sweep_pages._db.clear()
|
||||||
|
self._query._db.clear()
|
||||||
|
except AttributeError as ex:
|
||||||
|
logging.warning(f"Could not invalidate cache, maybe it was not used in {ex.args}?")
|
||||||
|
|
||||||
|
|
||||||
|
def insert_early_stage_jobs(stage_sequence: StageSeq, jobs_metadata: Dag) -> Dag:
|
||||||
|
pre_processed_dag: dict[str, set[str]] = {}
|
||||||
|
jobs_from_early_stages = list(accumulate(stage_sequence.values(), set.union))
|
||||||
|
for job_name, metadata in jobs_metadata.items():
|
||||||
|
final_needs: set[str] = deepcopy(metadata["needs"])
|
||||||
|
# Pre-process jobs that are not based on needs field
|
||||||
|
# e.g. sanity job in mesa MR pipelines
|
||||||
|
if not final_needs:
|
||||||
|
job_stage: str = jobs_metadata[job_name]["stage"]
|
||||||
|
stage_index: int = list(stage_sequence.keys()).index(job_stage)
|
||||||
|
if stage_index > 0:
|
||||||
|
final_needs |= jobs_from_early_stages[stage_index - 1]
|
||||||
|
pre_processed_dag[job_name] = final_needs
|
||||||
|
|
||||||
|
for job_name, needs in pre_processed_dag.items():
|
||||||
|
jobs_metadata[job_name]["needs"] = needs
|
||||||
|
|
||||||
|
return jobs_metadata
|
||||||
|
|
||||||
|
|
||||||
|
def traverse_dag_needs(jobs_metadata: Dag) -> None:
|
||||||
|
created_jobs = set(jobs_metadata.keys())
|
||||||
|
for job, metadata in jobs_metadata.items():
|
||||||
|
final_needs: set = deepcopy(metadata["needs"]) & created_jobs
|
||||||
|
# Post process jobs that are based on needs field
|
||||||
|
partial = True
|
||||||
|
|
||||||
|
while partial:
|
||||||
|
next_depth: set[str] = {n for dn in final_needs for n in jobs_metadata[dn]["needs"]}
|
||||||
|
partial: bool = not final_needs.issuperset(next_depth)
|
||||||
|
final_needs = final_needs.union(next_depth)
|
||||||
|
|
||||||
|
jobs_metadata[job]["needs"] = final_needs
|
||||||
|
|
||||||
|
|
||||||
|
def extract_stages_and_job_needs(
|
||||||
|
pipeline_jobs: dict[str, Any], pipeline_stages: dict[str, Any]
|
||||||
|
) -> tuple[StageSeq, Dag]:
|
||||||
|
jobs_metadata = Dag()
|
||||||
|
# Record the stage sequence to post process deps that are not based on needs
|
||||||
|
# field, for example: sanity job
|
||||||
|
stage_sequence: OrderedDict[str, set[str]] = OrderedDict()
|
||||||
|
for stage in pipeline_stages["nodes"]:
|
||||||
|
stage_sequence[stage["name"]] = set()
|
||||||
|
|
||||||
|
for job in pipeline_jobs["nodes"]:
|
||||||
|
stage_sequence[job["stage"]["name"]].add(job["name"])
|
||||||
|
dag_job: DagNode = {
|
||||||
|
"name": job["name"],
|
||||||
|
"stage": job["stage"]["name"],
|
||||||
|
"needs": set([j["node"]["name"] for j in job["needs"]["edges"]]),
|
||||||
|
}
|
||||||
|
jobs_metadata[job["name"]] = dag_job
|
||||||
|
|
||||||
|
return stage_sequence, jobs_metadata
|
||||||
|
|
||||||
|
|
||||||
|
def create_job_needs_dag(gl_gql: GitlabGQL, params, disable_cache: bool = True) -> Dag:
|
||||||
|
"""
|
||||||
|
This function creates a Directed Acyclic Graph (DAG) to represent a sequence of jobs, where each
|
||||||
|
job has a set of jobs that it depends on (its "needs") and belongs to a certain "stage".
|
||||||
|
The "name" of the job is used as the key in the dictionary.
|
||||||
|
|
||||||
|
For example, consider the following DAG:
|
||||||
|
|
||||||
|
1. build stage: job1 -> job2 -> job3
|
||||||
|
2. test stage: job2 -> job4
|
||||||
|
|
||||||
|
- The job needs for job3 are: job1, job2
|
||||||
|
- The job needs for job4 are: job2
|
||||||
|
- The job2 needs to wait all jobs from build stage to finish.
|
||||||
|
|
||||||
|
The resulting DAG would look like this:
|
||||||
|
|
||||||
|
dag = {
|
||||||
|
"job1": {"needs": set(), "stage": "build", "name": "job1"},
|
||||||
|
"job2": {"needs": {"job1", "job2", job3"}, "stage": "test", "name": "job2"},
|
||||||
|
"job3": {"needs": {"job1", "job2"}, "stage": "build", "name": "job3"},
|
||||||
|
"job4": {"needs": {"job2"}, "stage": "test", "name": "job4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
To access the job needs, one can do:
|
||||||
|
|
||||||
|
dag["job3"]["needs"]
|
||||||
|
|
||||||
|
This will return the set of jobs that job3 needs: {"job1", "job2"}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gl_gql (GitlabGQL): The `gl_gql` parameter is an instance of the `GitlabGQL` class, which is
|
||||||
|
used to make GraphQL queries to the GitLab API.
|
||||||
|
params (dict): The `params` parameter is a dictionary that contains the necessary parameters
|
||||||
|
for the GraphQL query. It is used to specify the details of the pipeline for which the
|
||||||
|
job needs DAG is being created.
|
||||||
|
The specific keys and values in the `params` dictionary will depend on
|
||||||
|
the requirements of the GraphQL query being executed
|
||||||
|
disable_cache (bool): The `disable_cache` parameter is a boolean that specifies whether the
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The final DAG (Directed Acyclic Graph) representing the job dependencies sourced from needs
|
||||||
|
or stages rule.
|
||||||
|
"""
|
||||||
|
stages_jobs_gql = gl_gql.query(
|
||||||
|
"pipeline_details.gql",
|
||||||
|
params=params,
|
||||||
|
paginated_key_loc=["project", "pipeline", "jobs"],
|
||||||
|
disable_cache=disable_cache,
|
||||||
|
)
|
||||||
|
pipeline_data = stages_jobs_gql["project"]["pipeline"]
|
||||||
|
if not pipeline_data:
|
||||||
|
raise RuntimeError(f"Could not find any pipelines for {params}")
|
||||||
|
|
||||||
|
stage_sequence, jobs_metadata = extract_stages_and_job_needs(
|
||||||
|
pipeline_data["jobs"], pipeline_data["stages"]
|
||||||
|
)
|
||||||
|
# Fill the DAG with the job needs from stages that don't have any needs but still need to wait
|
||||||
|
# for previous stages
|
||||||
|
final_dag = insert_early_stage_jobs(stage_sequence, jobs_metadata)
|
||||||
|
# Now that each job has its direct needs filled correctly, update the "needs" field for each job
|
||||||
|
# in the DAG by performing a topological traversal
|
||||||
|
traverse_dag_needs(final_dag)
|
||||||
|
|
||||||
|
return final_dag
|
||||||
|
|
||||||
|
|
||||||
|
def filter_dag(dag: Dag, regex: Pattern) -> Dag:
|
||||||
|
jobs_with_regex: set[str] = {job for job in dag if regex.fullmatch(job)}
|
||||||
|
return Dag({job: data for job, data in dag.items() if job in sorted(jobs_with_regex)})
|
||||||
|
|
||||||
|
|
||||||
|
def print_dag(dag: Dag) -> None:
|
||||||
|
for job, data in dag.items():
|
||||||
|
print(f"{job}:")
|
||||||
|
print(f"\t{' '.join(data['needs'])}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_merged_yaml(gl_gql: GitlabGQL, params) -> dict[str, Any]:
|
||||||
|
params["content"] = dedent("""\
|
||||||
|
include:
|
||||||
|
- local: .gitlab-ci.yml
|
||||||
|
""")
|
||||||
|
raw_response = gl_gql.query("job_details.gql", params)
|
||||||
|
if merged_yaml := raw_response["ciConfig"]["mergedYaml"]:
|
||||||
|
return yaml.safe_load(merged_yaml)
|
||||||
|
|
||||||
|
gl_gql.invalidate_query_cache()
|
||||||
|
raise ValueError(
|
||||||
|
"""
|
||||||
|
Could not fetch any content for merged YAML,
|
||||||
|
please verify if the git SHA exists in remote.
|
||||||
|
Maybe you forgot to `git push`? """
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def recursive_fill(job, relationship_field, target_data, acc_data: dict, merged_yaml):
|
||||||
|
if relatives := job.get(relationship_field):
|
||||||
|
if isinstance(relatives, str):
|
||||||
|
relatives = [relatives]
|
||||||
|
|
||||||
|
for relative in relatives:
|
||||||
|
parent_job = merged_yaml[relative]
|
||||||
|
acc_data = recursive_fill(parent_job, acc_data, merged_yaml) # type: ignore
|
||||||
|
|
||||||
|
acc_data |= job.get(target_data, {})
|
||||||
|
|
||||||
|
return acc_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_variables(job, merged_yaml, project_path, sha) -> dict[str, str]:
|
||||||
|
p = get_project_root_dir() / ".gitlab-ci" / "image-tags.yml"
|
||||||
|
image_tags = yaml.safe_load(p.read_text())
|
||||||
|
|
||||||
|
variables = image_tags["variables"]
|
||||||
|
variables |= merged_yaml["variables"]
|
||||||
|
variables |= job["variables"]
|
||||||
|
variables["CI_PROJECT_PATH"] = project_path
|
||||||
|
variables["CI_PROJECT_NAME"] = project_path.split("/")[1]
|
||||||
|
variables["CI_REGISTRY_IMAGE"] = "registry.freedesktop.org/${CI_PROJECT_PATH}"
|
||||||
|
variables["CI_COMMIT_SHA"] = sha
|
||||||
|
|
||||||
|
while recurse_among_variables_space(variables):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return variables
|
||||||
|
|
||||||
|
|
||||||
|
# Based on: https://stackoverflow.com/a/2158532/1079223
|
||||||
|
def flatten(xs):
|
||||||
|
for x in xs:
|
||||||
|
if isinstance(x, Iterable) and not isinstance(x, (str, bytes)):
|
||||||
|
yield from flatten(x)
|
||||||
|
else:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
|
||||||
|
def get_full_script(job) -> list[str]:
|
||||||
|
script = []
|
||||||
|
for script_part in ("before_script", "script", "after_script"):
|
||||||
|
script.append(f"# {script_part}")
|
||||||
|
lines = flatten(job.get(script_part, []))
|
||||||
|
script.extend(lines)
|
||||||
|
script.append("")
|
||||||
|
|
||||||
|
return script
|
||||||
|
|
||||||
|
|
||||||
|
def recurse_among_variables_space(var_graph) -> bool:
|
||||||
|
updated = False
|
||||||
|
for var, value in var_graph.items():
|
||||||
|
value = str(value)
|
||||||
|
dep_vars = []
|
||||||
|
if match := re.findall(r"(\$[{]?[\w\d_]*[}]?)", value):
|
||||||
|
all_dep_vars = [v.lstrip("${").rstrip("}") for v in match]
|
||||||
|
# print(value, match, all_dep_vars)
|
||||||
|
dep_vars = [v for v in all_dep_vars if v in var_graph]
|
||||||
|
|
||||||
|
for dep_var in dep_vars:
|
||||||
|
dep_value = str(var_graph[dep_var])
|
||||||
|
new_value = var_graph[var]
|
||||||
|
new_value = new_value.replace(f"${{{dep_var}}}", dep_value)
|
||||||
|
new_value = new_value.replace(f"${dep_var}", dep_value)
|
||||||
|
var_graph[var] = new_value
|
||||||
|
updated |= dep_value != new_value
|
||||||
|
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
def print_job_final_definition(job_name, merged_yaml, project_path, sha):
|
||||||
|
job = merged_yaml[job_name]
|
||||||
|
variables = get_variables(job, merged_yaml, project_path, sha)
|
||||||
|
|
||||||
|
print("# --------- variables ---------------")
|
||||||
|
for var, value in sorted(variables.items()):
|
||||||
|
print(f"export {var}={value!r}")
|
||||||
|
|
||||||
|
# TODO: Recurse into needs to get full script
|
||||||
|
# TODO: maybe create a extra yaml file to avoid too much rework
|
||||||
|
script = get_full_script(job)
|
||||||
|
print()
|
||||||
|
print()
|
||||||
|
print("# --------- full script ---------------")
|
||||||
|
print("\n".join(script))
|
||||||
|
|
||||||
|
if image := variables.get("MESA_IMAGE"):
|
||||||
|
print()
|
||||||
|
print()
|
||||||
|
print("# --------- container image ---------------")
|
||||||
|
print(image)
|
||||||
|
|
||||||
|
|
||||||
|
def from_sha_to_pipeline_iid(gl_gql: GitlabGQL, params) -> str:
|
||||||
|
result = gl_gql.query("pipeline_utils.gql", params)
|
||||||
|
|
||||||
|
return result["project"]["pipelines"]["nodes"][0]["iid"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> Namespace:
|
||||||
|
parser = ArgumentParser(
|
||||||
|
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||||
|
description="CLI and library with utility functions to debug jobs via Gitlab GraphQL",
|
||||||
|
epilog=f"""Example:
|
||||||
|
{Path(__file__).name} --print-dag""",
|
||||||
|
)
|
||||||
|
parser.add_argument("-pp", "--project-path", type=str, default="mesa/mesa")
|
||||||
|
parser.add_argument("--sha", "--rev", type=str, default='HEAD')
|
||||||
|
parser.add_argument(
|
||||||
|
"--regex",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Regex pattern for the job name to be considered",
|
||||||
|
)
|
||||||
|
mutex_group_print = parser.add_mutually_exclusive_group()
|
||||||
|
mutex_group_print.add_argument(
|
||||||
|
"--print-dag",
|
||||||
|
action="store_true",
|
||||||
|
help="Print job needs DAG",
|
||||||
|
)
|
||||||
|
mutex_group_print.add_argument(
|
||||||
|
"--print-merged-yaml",
|
||||||
|
action="store_true",
|
||||||
|
help="Print the resulting YAML for the specific SHA",
|
||||||
|
)
|
||||||
|
mutex_group_print.add_argument(
|
||||||
|
"--print-job-manifest",
|
||||||
|
metavar='JOB_NAME',
|
||||||
|
type=str,
|
||||||
|
help="Print the resulting job data"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--gitlab-token-file",
|
||||||
|
type=str,
|
||||||
|
default=get_token_from_default_dir(),
|
||||||
|
help="force GitLab token, otherwise it's read from $XDG_CONFIG_HOME/gitlab-token",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.gitlab_token = Path(args.gitlab_token_file).read_text().strip()
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
gl_gql = GitlabGQL(token=args.gitlab_token)
|
||||||
|
|
||||||
|
sha = check_output(['git', 'rev-parse', args.sha]).decode('ascii').strip()
|
||||||
|
|
||||||
|
if args.print_dag:
|
||||||
|
iid = from_sha_to_pipeline_iid(gl_gql, {"projectPath": args.project_path, "sha": sha})
|
||||||
|
dag = create_job_needs_dag(
|
||||||
|
gl_gql, {"projectPath": args.project_path, "iid": iid}, disable_cache=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.regex:
|
||||||
|
dag = filter_dag(dag, re.compile(args.regex))
|
||||||
|
|
||||||
|
print_dag(dag)
|
||||||
|
|
||||||
|
if args.print_merged_yaml or args.print_job_manifest:
|
||||||
|
merged_yaml = fetch_merged_yaml(
|
||||||
|
gl_gql, {"projectPath": args.project_path, "sha": sha}
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.print_merged_yaml:
|
||||||
|
print(yaml.dump(merged_yaml, indent=2))
|
||||||
|
|
||||||
|
if args.print_job_manifest:
|
||||||
|
print_job_final_definition(
|
||||||
|
args.print_job_manifest, merged_yaml, args.project_path, sha
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
10
bin/ci/gitlab_gql.sh
Executable file
10
bin/ci/gitlab_gql.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
this_dir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")
|
||||||
|
readonly this_dir
|
||||||
|
|
||||||
|
exec \
|
||||||
|
"$this_dir/../python-venv.sh" \
|
||||||
|
"$this_dir/requirements.txt" \
|
||||||
|
"$this_dir/gitlab_gql.py" "$@"
|
7
bin/ci/job_details.gql
Normal file
7
bin/ci/job_details.gql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
query getCiConfigData($projectPath: ID!, $sha: String, $content: String!) {
|
||||||
|
ciConfig(projectPath: $projectPath, sha: $sha, content: $content) {
|
||||||
|
errors
|
||||||
|
mergedYaml
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
67
bin/ci/marge_queue.py
Executable file
67
bin/ci/marge_queue.py
Executable file
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright © 2020 - 2023 Collabora Ltd.
|
||||||
|
# Authors:
|
||||||
|
# David Heidelberg <david.heidelberg@collabora.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Monitors Marge-bot and return number of assigned MRs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from dateutil import parser
|
||||||
|
|
||||||
|
import gitlab
|
||||||
|
from gitlab_common import read_token, pretty_duration
|
||||||
|
|
||||||
|
REFRESH_WAIT = 30
|
||||||
|
MARGE_BOT_USER_ID = 9716
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> None:
|
||||||
|
"""Parse args"""
|
||||||
|
parse = argparse.ArgumentParser(
|
||||||
|
description="Tool to show merge requests assigned to the marge-bot",
|
||||||
|
)
|
||||||
|
parse.add_argument(
|
||||||
|
"--wait", action="store_true", help="wait until CI is free",
|
||||||
|
)
|
||||||
|
parse.add_argument(
|
||||||
|
"--token",
|
||||||
|
metavar="token",
|
||||||
|
help="force GitLab token, otherwise it's read from ~/.config/gitlab-token",
|
||||||
|
)
|
||||||
|
return parse.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parse_args()
|
||||||
|
token = read_token(args.token)
|
||||||
|
gl = gitlab.Gitlab(url="https://gitlab.freedesktop.org", private_token=token)
|
||||||
|
|
||||||
|
project = gl.projects.get("mesa/mesa")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
mrs = project.mergerequests.list(assignee_id=MARGE_BOT_USER_ID, scope="all", state="opened", get_all=True)
|
||||||
|
|
||||||
|
jobs_num = len(mrs)
|
||||||
|
for mr in mrs:
|
||||||
|
updated = parser.parse(mr.updated_at)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
diff = (now - updated).total_seconds()
|
||||||
|
print(
|
||||||
|
f"⛭ \u001b]8;;{mr.web_url}\u001b\\{mr.title}\u001b]8;;\u001b\\ ({pretty_duration(diff)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Job waiting: " + str(jobs_num))
|
||||||
|
|
||||||
|
if jobs_num == 0:
|
||||||
|
sys.exit(0)
|
||||||
|
if not args.wait:
|
||||||
|
sys.exit(min(jobs_num, 127))
|
||||||
|
|
||||||
|
time.sleep(REFRESH_WAIT)
|
10
bin/ci/marge_queue.sh
Executable file
10
bin/ci/marge_queue.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
this_dir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")
|
||||||
|
readonly this_dir
|
||||||
|
|
||||||
|
exec \
|
||||||
|
"$this_dir/../python-venv.sh" \
|
||||||
|
"$this_dir/requirements.txt" \
|
||||||
|
"$this_dir/marge_queue.py" "$@"
|
35
bin/ci/pipeline_details.gql
Normal file
35
bin/ci/pipeline_details.gql
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
query jobs($projectPath: ID!, $iid: ID!, $cursor: String) {
|
||||||
|
project(fullPath: $projectPath) {
|
||||||
|
id
|
||||||
|
pipeline(iid: $iid) {
|
||||||
|
id
|
||||||
|
iid
|
||||||
|
complete
|
||||||
|
stages {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs(after: $cursor) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
count
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
needs {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
bin/ci/pipeline_utils.gql
Normal file
9
bin/ci/pipeline_utils.gql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
query sha2pipelineIID($projectPath: ID!, $sha: String!) {
|
||||||
|
project(fullPath: $projectPath) {
|
||||||
|
pipelines(last: 1, sha:$sha){
|
||||||
|
nodes {
|
||||||
|
iid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
bin/ci/requirements.txt
Normal file
8
bin/ci/requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
colorama==0.4.5
|
||||||
|
filecache==0.81
|
||||||
|
gql==3.4.0
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
python-gitlab==3.5.0
|
||||||
|
PyYAML==6.0.1
|
||||||
|
ruamel.yaml.clib==0.2.8
|
||||||
|
ruamel.yaml==0.17.21
|
294
bin/ci/structured_logger.py
Normal file
294
bin/ci/structured_logger.py
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
"""
|
||||||
|
A structured logging utility supporting multiple data formats such as CSV, JSON,
|
||||||
|
and YAML.
|
||||||
|
|
||||||
|
The main purpose of this script, besides having relevant information available
|
||||||
|
in a condensed and deserialized.
|
||||||
|
|
||||||
|
This script defines a protocol for different file handling strategies and provides
|
||||||
|
implementations for CSV, JSON, and YAML formats. The main class, StructuredLogger,
|
||||||
|
allows for easy interaction with log data, enabling users to load, save, increment,
|
||||||
|
set, and append fields in the log. The script also includes context managers for
|
||||||
|
file locking and editing log data to ensure data integrity and avoid race conditions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from collections.abc import MutableMapping, MutableSequence
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
import fire
|
||||||
|
from filelock import FileLock
|
||||||
|
|
||||||
|
try:
|
||||||
|
import polars as pl
|
||||||
|
|
||||||
|
CSV_LIB_EXCEPTION = None
|
||||||
|
except ImportError as e:
|
||||||
|
CSV_LIB_EXCEPTION: ImportError = e
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
YAML_LIB_EXCEPTION = None
|
||||||
|
except ImportError as e:
|
||||||
|
YAML_LIB_EXCEPTION: ImportError = e
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerProxy:
|
||||||
|
"""
|
||||||
|
A proxy class that wraps a mutable container object (such as a dictionary or
|
||||||
|
a list) and calls a provided save_callback function whenever the container
|
||||||
|
or its contents
|
||||||
|
are changed.
|
||||||
|
"""
|
||||||
|
def __init__(self, container, save_callback):
|
||||||
|
self.container = container
|
||||||
|
self.save_callback = save_callback
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
value = self.container[key]
|
||||||
|
if isinstance(value, (MutableMapping, MutableSequence)):
|
||||||
|
return ContainerProxy(value, self.save_callback)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.container[key] = value
|
||||||
|
self.save_callback()
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self.container[key]
|
||||||
|
self.save_callback()
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
attr = getattr(self.container, name)
|
||||||
|
|
||||||
|
if callable(attr):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
result = attr(*args, **kwargs)
|
||||||
|
self.save_callback()
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.container)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.container)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.container)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoSaveDict(dict):
|
||||||
|
"""
|
||||||
|
A subclass of the built-in dict class with additional functionality to
|
||||||
|
automatically save changes to the dictionary. It maintains a timestamp of
|
||||||
|
the last modification and automatically wraps nested mutable containers
|
||||||
|
using ContainerProxy.
|
||||||
|
"""
|
||||||
|
timestamp_key = "_timestamp"
|
||||||
|
|
||||||
|
def __init__(self, *args, save_callback, register_timestamp=True, **kwargs):
|
||||||
|
self.save_callback = save_callback
|
||||||
|
self.__register_timestamp = register_timestamp
|
||||||
|
self.__heartbeat()
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.__wrap_dictionaries()
|
||||||
|
|
||||||
|
def __heartbeat(self):
|
||||||
|
if self.__register_timestamp:
|
||||||
|
self[AutoSaveDict.timestamp_key] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
def __save(self):
|
||||||
|
self.__heartbeat()
|
||||||
|
self.save_callback()
|
||||||
|
|
||||||
|
def __wrap_dictionaries(self):
|
||||||
|
for key, value in self.items():
|
||||||
|
if isinstance(value, MutableMapping) and not isinstance(
|
||||||
|
value, AutoSaveDict
|
||||||
|
):
|
||||||
|
self[key] = AutoSaveDict(
|
||||||
|
value, save_callback=self.save_callback, register_timestamp=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if isinstance(value, MutableMapping) and not isinstance(value, AutoSaveDict):
|
||||||
|
value = AutoSaveDict(
|
||||||
|
value, save_callback=self.save_callback, register_timestamp=False
|
||||||
|
)
|
||||||
|
super().__setitem__(key, value)
|
||||||
|
|
||||||
|
if self.__register_timestamp and key == AutoSaveDict.timestamp_key:
|
||||||
|
return
|
||||||
|
self.__save()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
value = super().__getitem__(key)
|
||||||
|
if isinstance(value, (MutableMapping, MutableSequence)):
|
||||||
|
return ContainerProxy(value, self.__save)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
super().__delitem__(key)
|
||||||
|
self.__save()
|
||||||
|
|
||||||
|
def pop(self, *args, **kwargs):
|
||||||
|
result = super().pop(*args, **kwargs)
|
||||||
|
self.__save()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update(self, *args, **kwargs):
|
||||||
|
super().update(*args, **kwargs)
|
||||||
|
self.__wrap_dictionaries()
|
||||||
|
self.__save()
|
||||||
|
|
||||||
|
|
||||||
|
class StructuredLoggerStrategy(Protocol):
|
||||||
|
def load_data(self, file_path: Path) -> dict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def save_data(self, file_path: Path, data: dict) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CSVStrategy:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if CSV_LIB_EXCEPTION:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Can't parse CSV files. Missing library"
|
||||||
|
) from CSV_LIB_EXCEPTION
|
||||||
|
|
||||||
|
def load_data(self, file_path: Path) -> dict:
|
||||||
|
dicts: list[dict[str, Any]] = pl.read_csv(
|
||||||
|
file_path, try_parse_dates=True
|
||||||
|
).to_dicts()
|
||||||
|
data = {}
|
||||||
|
for d in dicts:
|
||||||
|
for k, v in d.items():
|
||||||
|
if k != AutoSaveDict.timestamp_key and k in data:
|
||||||
|
if isinstance(data[k], list):
|
||||||
|
data[k].append(v)
|
||||||
|
continue
|
||||||
|
data[k] = [data[k], v]
|
||||||
|
else:
|
||||||
|
data[k] = v
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save_data(self, file_path: Path, data: dict) -> None:
|
||||||
|
pl.DataFrame(data).write_csv(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONStrategy:
|
||||||
|
def load_data(self, file_path: Path) -> dict:
|
||||||
|
return json.loads(file_path.read_text())
|
||||||
|
|
||||||
|
def save_data(self, file_path: Path, data: dict) -> None:
|
||||||
|
with open(file_path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
class YAMLStrategy:
|
||||||
|
def __init__(self):
|
||||||
|
if YAML_LIB_EXCEPTION:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Can't parse YAML files. Missing library"
|
||||||
|
) from YAML_LIB_EXCEPTION
|
||||||
|
self.yaml = YAML()
|
||||||
|
self.yaml.indent(sequence=4, offset=2)
|
||||||
|
self.yaml.default_flow_style = False
|
||||||
|
self.yaml.representer.add_representer(AutoSaveDict, self.represent_dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def represent_dict(cls, dumper, data):
|
||||||
|
return dumper.represent_mapping("tag:yaml.org,2002:map", data)
|
||||||
|
|
||||||
|
def load_data(self, file_path: Path) -> dict:
|
||||||
|
return self.yaml.load(file_path.read_text())
|
||||||
|
|
||||||
|
def save_data(self, file_path: Path, data: dict) -> None:
|
||||||
|
with open(file_path, "w") as f:
|
||||||
|
self.yaml.dump(data, f)
|
||||||
|
|
||||||
|
|
||||||
|
class StructuredLogger:
|
||||||
|
def __init__(
|
||||||
|
self, file_name: str, strategy: StructuredLoggerStrategy = None, truncate=False
|
||||||
|
):
|
||||||
|
self.file_name: str = file_name
|
||||||
|
self.file_path = Path(self.file_name)
|
||||||
|
self._data: AutoSaveDict = AutoSaveDict(save_callback=self.save_data)
|
||||||
|
|
||||||
|
if strategy is None:
|
||||||
|
self.strategy: StructuredLoggerStrategy = self.guess_strategy_from_file(
|
||||||
|
self.file_path
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
if not self.file_path.exists():
|
||||||
|
Path.mkdir(self.file_path.parent, exist_ok=True)
|
||||||
|
self.save_data()
|
||||||
|
return
|
||||||
|
|
||||||
|
if truncate:
|
||||||
|
with self.get_lock():
|
||||||
|
os.truncate(self.file_path, 0)
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
self._data = self.strategy.load_data(self.file_path)
|
||||||
|
|
||||||
|
def save_data(self):
|
||||||
|
self.strategy.save_data(self.file_path, self._data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> AutoSaveDict:
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_lock(self):
|
||||||
|
with FileLock(f"{self.file_path}.lock", timeout=10):
|
||||||
|
yield
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def edit_context(self):
|
||||||
|
"""
|
||||||
|
Context manager that ensures proper loading and saving of log data when
|
||||||
|
performing multiple modifications.
|
||||||
|
"""
|
||||||
|
with self.get_lock():
|
||||||
|
try:
|
||||||
|
self.load_data()
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def guess_strategy_from_file(file_path: Path) -> StructuredLoggerStrategy:
|
||||||
|
file_extension = file_path.suffix.lower().lstrip(".")
|
||||||
|
return StructuredLogger.get_strategy(file_extension)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_strategy(strategy_name: str) -> StructuredLoggerStrategy:
|
||||||
|
strategies = {
|
||||||
|
"csv": CSVStrategy,
|
||||||
|
"json": JSONStrategy,
|
||||||
|
"yaml": YAMLStrategy,
|
||||||
|
"yml": YAMLStrategy,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return strategies[strategy_name]()
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f"Unknown strategy for: {strategy_name}") from e
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fire.Fire(StructuredLogger)
|
5
bin/ci/test/requirements.txt
Normal file
5
bin/ci/test/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
filelock==3.12.4
|
||||||
|
fire==0.5.0
|
||||||
|
mock==5.1.0
|
||||||
|
polars==0.19.3
|
||||||
|
pytest==7.4.2
|
669
bin/ci/test/test_custom_logger.py
Normal file
669
bin/ci/test/test_custom_logger.py
Normal file
|
@ -0,0 +1,669 @@
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from custom_logger import CustomLogger
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tmp_log_file(tmp_path):
|
||||||
|
return tmp_path / "test_log.json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def custom_logger(tmp_log_file):
|
||||||
|
return CustomLogger(tmp_log_file)
|
||||||
|
|
||||||
|
|
||||||
|
def run_script_with_args(args):
|
||||||
|
import custom_logger
|
||||||
|
|
||||||
|
script_path = custom_logger.__file__
|
||||||
|
return subprocess.run(
|
||||||
|
["python3", str(script_path), *args], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for missing log file
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"key, value", [("dut_attempt_counter", "1"), ("job_combined_status", "pass")]
|
||||||
|
)
|
||||||
|
def test_missing_log_file_argument(key, value):
|
||||||
|
result = run_script_with_args(["--update", "key", "value"])
|
||||||
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
||||||
|
# Parametrize test case for valid update arguments
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"key, value", [("dut_attempt_counter", "1"), ("job_combined_status", "pass")]
|
||||||
|
)
|
||||||
|
def test_update_argument_valid(custom_logger, tmp_log_file, key, value):
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--update", key, value])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for passing only the key without a value
|
||||||
|
def test_update_argument_key_only(custom_logger, tmp_log_file):
|
||||||
|
key = "dut_attempt_counter"
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--update", key])
|
||||||
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for not passing any key-value pair
|
||||||
|
def test_update_argument_no_values(custom_logger, tmp_log_file):
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--update"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Parametrize test case for valid arguments
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"key, value", [("dut_attempt_counter", "1"), ("job_combined_status", "pass")]
|
||||||
|
)
|
||||||
|
def test_create_argument_valid(custom_logger, tmp_log_file, key, value):
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--create-dut-job", key, value])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for passing only the key without a value
|
||||||
|
def test_create_argument_key_only(custom_logger, tmp_log_file):
|
||||||
|
key = "dut_attempt_counter"
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--create-dut-job", key])
|
||||||
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for not passing any key-value pair
|
||||||
|
def test_create_argument_no_values(custom_logger, tmp_log_file):
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--create-dut-job"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating a DUT job
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"key, value", [("status", "hung"), ("dut_state", "Canceling"), ("dut_name", "asus")]
|
||||||
|
)
|
||||||
|
def test_update_dut_job(custom_logger, tmp_log_file, key, value):
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--update-dut-job", key, value])
|
||||||
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--create-dut-job", key, value])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--update-dut-job", key, value])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating last DUT job
|
||||||
|
def test_update_dut_multiple_job(custom_logger, tmp_log_file):
|
||||||
|
# Create the first DUT job with the first key
|
||||||
|
result = run_script_with_args(
|
||||||
|
[str(tmp_log_file), "--create-dut-job", "status", "hung"]
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
# Create the second DUT job with the second key
|
||||||
|
result = run_script_with_args(
|
||||||
|
[str(tmp_log_file), "--create-dut-job", "dut_state", "Canceling"]
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
result = run_script_with_args(
|
||||||
|
[str(tmp_log_file), "--update-dut-job", "dut_name", "asus"]
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Parametrize test case for valid phase arguments
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"phase_name",
|
||||||
|
[("Phase1"), ("Phase2"), ("Phase3")],
|
||||||
|
)
|
||||||
|
def test_create_job_phase_valid(custom_logger, tmp_log_file, phase_name):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--create-job-phase", phase_name])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for not passing any arguments for create-job-phase
|
||||||
|
def test_create_job_phase_no_arguments(custom_logger, tmp_log_file):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--create-job-phase"])
|
||||||
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for trying to create a phase job without an existing DUT job
|
||||||
|
def test_create_job_phase_no_dut_job(custom_logger, tmp_log_file):
|
||||||
|
phase_name = "Phase1"
|
||||||
|
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--create-job-phase", phase_name])
|
||||||
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
||||||
|
# Combined test cases for valid scenarios
|
||||||
|
def test_valid_scenarios(custom_logger, tmp_log_file):
|
||||||
|
valid_update_args = [("dut_attempt_counter", "1"), ("job_combined_status", "pass")]
|
||||||
|
for key, value in valid_update_args:
|
||||||
|
result = run_script_with_args([str(tmp_log_file), "--update", key, value])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
valid_create_args = [
|
||||||
|
("status", "hung"),
|
||||||
|
("dut_state", "Canceling"),
|
||||||
|
("dut_name", "asus"),
|
||||||
|
("phase_name", "Bootloader"),
|
||||||
|
]
|
||||||
|
for key, value in valid_create_args:
|
||||||
|
result = run_script_with_args(
|
||||||
|
[str(tmp_log_file), "--create-dut-job", key, value]
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
result = run_script_with_args(
|
||||||
|
[str(tmp_log_file), "--create-dut-job", "status", "hung"]
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
result = run_script_with_args(
|
||||||
|
[str(tmp_log_file), "--update-dut-job", "dut_name", "asus"]
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
result = run_script_with_args(
|
||||||
|
[
|
||||||
|
str(tmp_log_file),
|
||||||
|
"--create-job-phase",
|
||||||
|
"phase_name",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
# Parametrize test case for valid update arguments
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"key, value", [("dut_attempt_counter", "1"), ("job_combined_status", "pass")]
|
||||||
|
)
|
||||||
|
def test_update(custom_logger, key, value):
|
||||||
|
custom_logger.update(**{key: value})
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert key in logger_data
|
||||||
|
assert logger_data[key] == value
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating with a key that already exists
|
||||||
|
def test_update_existing_key(custom_logger):
|
||||||
|
key = "status"
|
||||||
|
value = "new_value"
|
||||||
|
custom_logger.logger.data[key] = "old_value"
|
||||||
|
custom_logger.update(**{key: value})
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert key in logger_data
|
||||||
|
assert logger_data[key] == value
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating "dut_jobs"
|
||||||
|
def test_update_dut_jobs(custom_logger):
|
||||||
|
key1 = "status"
|
||||||
|
value1 = "fail"
|
||||||
|
key2 = "state"
|
||||||
|
value2 = "hung"
|
||||||
|
|
||||||
|
custom_logger.create_dut_job(**{key1: value1})
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
job1 = logger_data["dut_jobs"][0]
|
||||||
|
assert key1 in job1
|
||||||
|
assert job1[key1] == value1
|
||||||
|
|
||||||
|
custom_logger.update_dut_job(key2, value2)
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
job2 = logger_data["dut_jobs"][0]
|
||||||
|
assert key2 in job2
|
||||||
|
assert job2[key2] == value2
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for creating and updating DUT job
|
||||||
|
def test_create_dut_job(custom_logger):
|
||||||
|
key = "status"
|
||||||
|
value1 = "pass"
|
||||||
|
value2 = "fail"
|
||||||
|
value3 = "hung"
|
||||||
|
|
||||||
|
reason = "job_combined_status"
|
||||||
|
result = "Finished"
|
||||||
|
|
||||||
|
custom_logger.update(**{reason: result})
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert reason in logger_data
|
||||||
|
assert logger_data[reason] == result
|
||||||
|
|
||||||
|
# Create the first DUT job
|
||||||
|
custom_logger.create_dut_job(**{key: value1})
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert isinstance(logger_data["dut_jobs"], list)
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
assert isinstance(logger_data["dut_jobs"][0], dict)
|
||||||
|
|
||||||
|
# Check the values of the keys in the created first DUT job
|
||||||
|
job1 = logger_data["dut_jobs"][0]
|
||||||
|
assert key in job1
|
||||||
|
assert job1[key] == value1
|
||||||
|
|
||||||
|
# Create the second DUT job
|
||||||
|
custom_logger.create_dut_job(**{key: value2})
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert isinstance(logger_data["dut_jobs"], list)
|
||||||
|
assert len(logger_data["dut_jobs"]) == 2
|
||||||
|
assert isinstance(logger_data["dut_jobs"][1], dict)
|
||||||
|
|
||||||
|
# Check the values of the keys in the created second DUT job
|
||||||
|
job2 = logger_data["dut_jobs"][1]
|
||||||
|
assert key in job2
|
||||||
|
assert job2[key] == value2
|
||||||
|
|
||||||
|
# Update the second DUT job with value3
|
||||||
|
custom_logger.update_dut_job(key, value3)
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
# Check the updated value in the second DUT job
|
||||||
|
job2 = logger_data["dut_jobs"][1]
|
||||||
|
assert key in job2
|
||||||
|
assert job2[key] == value3
|
||||||
|
|
||||||
|
# Find the index of the last DUT job
|
||||||
|
last_job_index = len(logger_data["dut_jobs"]) - 1
|
||||||
|
|
||||||
|
# Update the last DUT job
|
||||||
|
custom_logger.update_dut_job("dut_name", "asus")
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
# Check the updated value in the last DUT job
|
||||||
|
job2 = logger_data["dut_jobs"][last_job_index]
|
||||||
|
assert "dut_name" in job2
|
||||||
|
assert job2["dut_name"] == "asus"
|
||||||
|
|
||||||
|
# Check that "dut_name" is not present in other DUT jobs
|
||||||
|
for idx, job in enumerate(logger_data["dut_jobs"]):
|
||||||
|
if idx != last_job_index:
|
||||||
|
assert job.get("dut_name") == ""
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating with missing "dut_jobs" key
|
||||||
|
def test_update_dut_job_missing_dut_jobs(custom_logger):
|
||||||
|
key = "status"
|
||||||
|
value = "fail"
|
||||||
|
|
||||||
|
# Attempt to update a DUT job when "dut_jobs" is missing
|
||||||
|
with pytest.raises(ValueError, match="No DUT jobs found."):
|
||||||
|
custom_logger.update_dut_job(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for creating a job phase
|
||||||
|
def test_create_job_phase(custom_logger):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
phase_name = "Phase1"
|
||||||
|
|
||||||
|
custom_logger.create_job_phase(phase_name)
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert isinstance(logger_data["dut_jobs"], list)
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
job = logger_data["dut_jobs"][0]
|
||||||
|
assert "dut_job_phases" in job
|
||||||
|
assert isinstance(job["dut_job_phases"], list)
|
||||||
|
assert len(job["dut_job_phases"]) == 1
|
||||||
|
|
||||||
|
phase = job["dut_job_phases"][0]
|
||||||
|
assert phase["name"] == phase_name
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(phase["start_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
assert phase["end_time"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for creating multiple phase jobs
|
||||||
|
def test_create_multiple_phase_jobs(custom_logger):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
|
||||||
|
phase_data = [
|
||||||
|
{
|
||||||
|
"phase_name": "Phase1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase_name": "Phase2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase_name": "Phase3",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for data in phase_data:
|
||||||
|
phase_name = data["phase_name"]
|
||||||
|
|
||||||
|
custom_logger.create_job_phase(phase_name)
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert isinstance(logger_data["dut_jobs"], list)
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
job = logger_data["dut_jobs"][0]
|
||||||
|
assert "dut_job_phases" in job
|
||||||
|
assert isinstance(job["dut_job_phases"], list)
|
||||||
|
assert len(job["dut_job_phases"]) == len(phase_data)
|
||||||
|
|
||||||
|
for data in phase_data:
|
||||||
|
phase_name = data["phase_name"]
|
||||||
|
|
||||||
|
phase = job["dut_job_phases"][phase_data.index(data)]
|
||||||
|
|
||||||
|
assert phase["name"] == phase_name
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(phase["start_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
if phase_data.index(data) != len(phase_data) - 1:
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(phase["end_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
# Check if the end_time of the last phase is an empty string
|
||||||
|
last_phase = job["dut_job_phases"][-1]
|
||||||
|
assert last_phase["end_time"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for creating multiple dut jobs and updating phase job for last dut job
|
||||||
|
def test_create_two_dut_jobs_and_add_phase(custom_logger):
|
||||||
|
# Create the first DUT job
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
|
||||||
|
# Create the second DUT job
|
||||||
|
custom_logger.create_dut_job(status="fail")
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert isinstance(logger_data["dut_jobs"], list)
|
||||||
|
assert len(logger_data["dut_jobs"]) == 2
|
||||||
|
|
||||||
|
first_dut_job = logger_data["dut_jobs"][0]
|
||||||
|
second_dut_job = logger_data["dut_jobs"][1]
|
||||||
|
|
||||||
|
# Add a phase to the second DUT job
|
||||||
|
custom_logger.create_job_phase("Phase1")
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert isinstance(logger_data["dut_jobs"], list)
|
||||||
|
assert len(logger_data["dut_jobs"]) == 2
|
||||||
|
|
||||||
|
first_dut_job = logger_data["dut_jobs"][0]
|
||||||
|
second_dut_job = logger_data["dut_jobs"][1]
|
||||||
|
|
||||||
|
# Check first DUT job does not have a phase
|
||||||
|
assert not first_dut_job.get("dut_job_phases")
|
||||||
|
|
||||||
|
# Check second DUT job has a phase
|
||||||
|
assert second_dut_job.get("dut_job_phases")
|
||||||
|
assert isinstance(second_dut_job["dut_job_phases"], list)
|
||||||
|
assert len(second_dut_job["dut_job_phases"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating DUT start time
|
||||||
|
def test_update_dut_start_time(custom_logger):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
custom_logger.update_dut_time("start", None)
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
dut_job = logger_data["dut_jobs"][0]
|
||||||
|
assert "dut_start_time" in dut_job
|
||||||
|
assert dut_job["dut_start_time"] != ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(dut_job["dut_start_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating DUT submit time
|
||||||
|
def test_update_dut_submit_time(custom_logger):
|
||||||
|
custom_time = "2023-11-09T02:37:06Z"
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
custom_logger.update_dut_time("submit", custom_time)
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
dut_job = logger_data["dut_jobs"][0]
|
||||||
|
assert "dut_submit_time" in dut_job
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(dut_job["dut_submit_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating DUT end time
|
||||||
|
def test_update_dut_end_time(custom_logger):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
custom_logger.update_dut_time("end", None)
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
dut_job = logger_data["dut_jobs"][0]
|
||||||
|
assert "dut_end_time" in dut_job
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(dut_job["dut_end_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating DUT time with invalid value
|
||||||
|
def test_update_dut_time_invalid_value(custom_logger):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match="Error: Invalid argument provided for --update-dut-time. Use 'start', 'submit', 'end'.",
|
||||||
|
):
|
||||||
|
custom_logger.update_dut_time("invalid_value", None)
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for close_dut_job
|
||||||
|
def test_close_dut_job(custom_logger):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
|
||||||
|
custom_logger.create_job_phase("Phase1")
|
||||||
|
custom_logger.create_job_phase("Phase2")
|
||||||
|
|
||||||
|
custom_logger.close_dut_job()
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
dut_job = logger_data["dut_jobs"][0]
|
||||||
|
assert "dut_job_phases" in dut_job
|
||||||
|
dut_job_phases = dut_job["dut_job_phases"]
|
||||||
|
|
||||||
|
phase1 = dut_job_phases[0]
|
||||||
|
assert phase1["name"] == "Phase1"
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(phase1["start_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(phase1["end_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
phase2 = dut_job_phases[1]
|
||||||
|
assert phase2["name"] == "Phase2"
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(phase2["start_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(phase2["end_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for close
|
||||||
|
def test_close(custom_logger):
|
||||||
|
custom_logger.create_dut_job(status="pass")
|
||||||
|
|
||||||
|
custom_logger.close()
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
assert "dut_attempt_counter" in logger_data
|
||||||
|
assert logger_data["dut_attempt_counter"] == len(logger_data["dut_jobs"])
|
||||||
|
assert "job_combined_status" in logger_data
|
||||||
|
assert logger_data["job_combined_status"] != ""
|
||||||
|
|
||||||
|
dut_job = logger_data["dut_jobs"][0]
|
||||||
|
assert "submitter_end_time" in dut_job
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(dut_job["submitter_end_time"])
|
||||||
|
assert True
|
||||||
|
except ValueError:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating status to fail with a reason
|
||||||
|
def test_update_status_fail_with_reason(custom_logger):
|
||||||
|
custom_logger.create_dut_job()
|
||||||
|
|
||||||
|
reason = "kernel panic"
|
||||||
|
custom_logger.update_status_fail(reason)
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
dut_job = logger_data["dut_jobs"][0]
|
||||||
|
assert "status" in dut_job
|
||||||
|
assert dut_job["status"] == "fail"
|
||||||
|
assert "dut_job_fail_reason" in dut_job
|
||||||
|
assert dut_job["dut_job_fail_reason"] == reason
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for updating status to fail without providing a reason
|
||||||
|
def test_update_status_fail_without_reason(custom_logger):
|
||||||
|
custom_logger.create_dut_job()
|
||||||
|
|
||||||
|
custom_logger.update_status_fail()
|
||||||
|
|
||||||
|
# Check if the status is updated and fail reason is empty
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
dut_job = logger_data["dut_jobs"][0]
|
||||||
|
assert "status" in dut_job
|
||||||
|
assert dut_job["status"] == "fail"
|
||||||
|
assert "dut_job_fail_reason" in dut_job
|
||||||
|
assert dut_job["dut_job_fail_reason"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for check_dut_timings with submission time earlier than start time
|
||||||
|
def test_check_dut_timings_submission_earlier_than_start(custom_logger, caplog):
|
||||||
|
custom_logger.create_dut_job()
|
||||||
|
|
||||||
|
# Set submission time to be earlier than start time
|
||||||
|
custom_logger.update_dut_time("start", "2023-01-01T11:00:00")
|
||||||
|
custom_logger.update_dut_time("submit", "2023-01-01T12:00:00")
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
job = logger_data["dut_jobs"][0]
|
||||||
|
|
||||||
|
# Call check_dut_timings
|
||||||
|
custom_logger.check_dut_timings(job)
|
||||||
|
|
||||||
|
# Check if an error message is logged
|
||||||
|
assert "Job submission is happening before job start." in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for check_dut_timings with end time earlier than start time
|
||||||
|
def test_check_dut_timings_end_earlier_than_start(custom_logger, caplog):
|
||||||
|
custom_logger.create_dut_job()
|
||||||
|
|
||||||
|
# Set end time to be earlier than start time
|
||||||
|
custom_logger.update_dut_time("end", "2023-01-01T11:00:00")
|
||||||
|
custom_logger.update_dut_time("start", "2023-01-01T12:00:00")
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
job = logger_data["dut_jobs"][0]
|
||||||
|
|
||||||
|
# Call check_dut_timings
|
||||||
|
custom_logger.check_dut_timings(job)
|
||||||
|
|
||||||
|
# Check if an error message is logged
|
||||||
|
assert "Job ended before it started." in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
# Test case for check_dut_timings with valid timing sequence
|
||||||
|
def test_check_dut_timings_valid_timing_sequence(custom_logger, caplog):
|
||||||
|
custom_logger.create_dut_job()
|
||||||
|
|
||||||
|
# Set valid timing sequence
|
||||||
|
custom_logger.update_dut_time("submit", "2023-01-01T12:00:00")
|
||||||
|
custom_logger.update_dut_time("start", "2023-01-01T12:30:00")
|
||||||
|
custom_logger.update_dut_time("end", "2023-01-01T13:00:00")
|
||||||
|
|
||||||
|
logger_data = custom_logger.logger.data
|
||||||
|
assert "dut_jobs" in logger_data
|
||||||
|
assert len(logger_data["dut_jobs"]) == 1
|
||||||
|
|
||||||
|
job = logger_data["dut_jobs"][0]
|
||||||
|
|
||||||
|
# Call check_dut_timings
|
||||||
|
custom_logger.check_dut_timings(job)
|
||||||
|
|
||||||
|
# Check that no error messages are logged
|
||||||
|
assert "Job submission is happening before job start." not in caplog.text
|
||||||
|
assert "Job ended before it started." not in caplog.text
|
182
bin/ci/test/test_structured_logger.py
Normal file
182
bin/ci/test/test_structured_logger.py
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mock import MagicMock, patch
|
||||||
|
from structured_logger import (
|
||||||
|
AutoSaveDict,
|
||||||
|
CSVStrategy,
|
||||||
|
JSONStrategy,
|
||||||
|
StructuredLogger,
|
||||||
|
YAMLStrategy,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=[CSVStrategy, JSONStrategy, YAMLStrategy])
|
||||||
|
def strategy(request):
|
||||||
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def file_extension(strategy):
|
||||||
|
if strategy == CSVStrategy:
|
||||||
|
return "csv"
|
||||||
|
elif strategy == JSONStrategy:
|
||||||
|
return "json"
|
||||||
|
elif strategy == YAMLStrategy:
|
||||||
|
return "yaml"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tmp_file(tmp_path):
|
||||||
|
return tmp_path / "test.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_guess_strategy_from_file(tmp_path, strategy, file_extension):
|
||||||
|
file_name = tmp_path / f"test_guess.{file_extension}"
|
||||||
|
Path(file_name).touch()
|
||||||
|
guessed_strategy = StructuredLogger.guess_strategy_from_file(file_name)
|
||||||
|
assert isinstance(guessed_strategy, strategy)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_strategy(strategy, file_extension):
|
||||||
|
result = StructuredLogger.get_strategy(file_extension)
|
||||||
|
assert isinstance(result, strategy)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_file_extension(tmp_path):
|
||||||
|
file_name = tmp_path / "test_invalid.xyz"
|
||||||
|
Path(file_name).touch()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Unknown strategy for: xyz"):
|
||||||
|
StructuredLogger.guess_strategy_from_file(file_name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_existent_file(tmp_path, strategy, file_extension):
|
||||||
|
file_name = tmp_path / f"non_existent.{file_extension}"
|
||||||
|
logger = StructuredLogger(file_name, strategy())
|
||||||
|
|
||||||
|
assert logger.file_path.exists()
|
||||||
|
assert "_timestamp" in logger._data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def structured_logger_module():
|
||||||
|
with patch.dict("sys.modules", {"polars": None, "ruamel.yaml": None}):
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
import structured_logger
|
||||||
|
|
||||||
|
importlib.reload(structured_logger)
|
||||||
|
yield structured_logger
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_csv_library(tmp_path, structured_logger_module):
|
||||||
|
with pytest.raises(RuntimeError, match="Can't parse CSV files. Missing library"):
|
||||||
|
structured_logger_module.CSVStrategy()
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_yaml_library(tmp_path, structured_logger_module):
|
||||||
|
with pytest.raises(RuntimeError, match="Can't parse YAML files. Missing library"):
|
||||||
|
structured_logger_module.YAMLStrategy()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autosavedict_setitem():
|
||||||
|
save_callback = MagicMock()
|
||||||
|
d = AutoSaveDict(save_callback=save_callback)
|
||||||
|
d["key"] = "value"
|
||||||
|
assert d["key"] == "value"
|
||||||
|
save_callback.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autosavedict_delitem():
|
||||||
|
save_callback = MagicMock()
|
||||||
|
d = AutoSaveDict({"key": "value"}, save_callback=save_callback)
|
||||||
|
del d["key"]
|
||||||
|
assert "key" not in d
|
||||||
|
save_callback.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autosavedict_pop():
|
||||||
|
save_callback = MagicMock()
|
||||||
|
d = AutoSaveDict({"key": "value"}, save_callback=save_callback)
|
||||||
|
result = d.pop("key")
|
||||||
|
assert result == "value"
|
||||||
|
assert "key" not in d
|
||||||
|
save_callback.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autosavedict_update():
|
||||||
|
save_callback = MagicMock()
|
||||||
|
d = AutoSaveDict({"key": "old_value"}, save_callback=save_callback)
|
||||||
|
d.update({"key": "new_value"})
|
||||||
|
assert d["key"] == "new_value"
|
||||||
|
save_callback.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_structured_logger_setitem(tmp_file):
|
||||||
|
logger = StructuredLogger(tmp_file, JSONStrategy())
|
||||||
|
logger.data["field"] = "value"
|
||||||
|
|
||||||
|
with open(tmp_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert data["field"] == "value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_structured_logger_set_recursive(tmp_file):
|
||||||
|
logger = StructuredLogger(tmp_file, JSONStrategy())
|
||||||
|
logger.data["field"] = {"test": True}
|
||||||
|
other = logger.data["field"]
|
||||||
|
other["late"] = True
|
||||||
|
|
||||||
|
with open(tmp_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert data["field"]["test"]
|
||||||
|
assert data["field"]["late"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_structured_logger_set_list(tmp_file):
|
||||||
|
logger = StructuredLogger(tmp_file, JSONStrategy())
|
||||||
|
logger.data["field"] = [True]
|
||||||
|
other = logger.data["field"]
|
||||||
|
other.append(True)
|
||||||
|
|
||||||
|
with open(tmp_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert data["field"][0]
|
||||||
|
assert data["field"][1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_structured_logger_delitem(tmp_file):
|
||||||
|
logger = StructuredLogger(tmp_file, JSONStrategy())
|
||||||
|
logger.data["field"] = "value"
|
||||||
|
del logger.data["field"]
|
||||||
|
|
||||||
|
with open(tmp_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert "field" not in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_structured_logger_pop(tmp_file):
|
||||||
|
logger = StructuredLogger(tmp_file, JSONStrategy())
|
||||||
|
logger.data["field"] = "value"
|
||||||
|
logger.data.pop("field")
|
||||||
|
|
||||||
|
with open(tmp_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert "field" not in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_structured_logger_update(tmp_file):
|
||||||
|
logger = StructuredLogger(tmp_file, JSONStrategy())
|
||||||
|
logger.data.update({"field": "value"})
|
||||||
|
|
||||||
|
with open(tmp_file, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
assert data["field"] == "value"
|
143
bin/ci/update_traces_checksum.py
Executable file
143
bin/ci/update_traces_checksum.py
Executable file
|
@ -0,0 +1,143 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright © 2022 Collabora Ltd.
|
||||||
|
# Authors:
|
||||||
|
# David Heidelberg <david.heidelberg@collabora.com>
|
||||||
|
#
|
||||||
|
# For the dependencies, see the requirements.txt
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Helper script to update traces checksums
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import bz2
|
||||||
|
import glob
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
import gitlab
|
||||||
|
from colorama import Fore, Style
|
||||||
|
from gitlab_common import get_gitlab_project, read_token, wait_for_pipeline
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTION_FILE = "export PIGLIT_REPLAY_DESCRIPTION_FILE='.*/install/(.*)'$"
|
||||||
|
DEVICE_NAME = "export PIGLIT_REPLAY_DEVICE_NAME='(.*)'$"
|
||||||
|
|
||||||
|
|
||||||
|
def gather_results(
|
||||||
|
project,
|
||||||
|
pipeline,
|
||||||
|
) -> None:
|
||||||
|
"""Gather results"""
|
||||||
|
|
||||||
|
target_jobs_regex = re.compile(".*-traces([:].*)?$")
|
||||||
|
|
||||||
|
for job in pipeline.jobs.list(all=True, sort="desc"):
|
||||||
|
if target_jobs_regex.match(job.name) and job.status == "failed":
|
||||||
|
cur_job = project.jobs.get(job.id)
|
||||||
|
# get variables
|
||||||
|
print(f"👁 {job.name}...")
|
||||||
|
log: list[str] = cur_job.trace().decode("unicode_escape").splitlines()
|
||||||
|
filename: str = ''
|
||||||
|
dev_name: str = ''
|
||||||
|
for logline in log:
|
||||||
|
desc_file = re.search(DESCRIPTION_FILE, logline)
|
||||||
|
device_name = re.search(DEVICE_NAME, logline)
|
||||||
|
if desc_file:
|
||||||
|
filename = desc_file.group(1)
|
||||||
|
if device_name:
|
||||||
|
dev_name = device_name.group(1)
|
||||||
|
|
||||||
|
if not filename or not dev_name:
|
||||||
|
print(Fore.RED + "Couldn't find device name or YML file in the logs!" + Style.RESET_ALL)
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"👁 Found {dev_name} and file {filename}")
|
||||||
|
|
||||||
|
# find filename in Mesa source
|
||||||
|
traces_file = glob.glob('./**/' + filename, recursive=True)
|
||||||
|
# write into it
|
||||||
|
with open(traces_file[0], 'r', encoding='utf-8') as target_file:
|
||||||
|
yaml = YAML()
|
||||||
|
yaml.compact(seq_seq=False, seq_map=False)
|
||||||
|
yaml.version = 1,2
|
||||||
|
yaml.width = 2048 # do not break the text fields
|
||||||
|
yaml.default_flow_style = None
|
||||||
|
target = yaml.load(target_file)
|
||||||
|
|
||||||
|
# parse artifact
|
||||||
|
results_json_bz2 = cur_job.artifact(path="results/results.json.bz2", streamed=False)
|
||||||
|
results_json = bz2.decompress(results_json_bz2).decode("utf-8", errors="replace")
|
||||||
|
results = json.loads(results_json)
|
||||||
|
|
||||||
|
for _, value in results["tests"].items():
|
||||||
|
if (
|
||||||
|
not value['images'] or
|
||||||
|
not value['images'][0] or
|
||||||
|
"image_desc" not in value['images'][0]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
trace: str = value['images'][0]['image_desc']
|
||||||
|
checksum: str = value['images'][0]['checksum_render']
|
||||||
|
|
||||||
|
if not checksum:
|
||||||
|
print(Fore.RED + f"{dev_name}: {trace}: checksum is missing! Crash?" + Style.RESET_ALL)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if checksum == "error":
|
||||||
|
print(Fore.RED + f"{dev_name}: {trace}: crashed" + Style.RESET_ALL)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target['traces'][trace][dev_name].get('checksum') == checksum:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "label" in target['traces'][trace][dev_name]:
|
||||||
|
print(f'{dev_name}: {trace}: please verify that label {Fore.BLUE}{target["traces"][trace][dev_name]["label"]}{Style.RESET_ALL} is still valid')
|
||||||
|
|
||||||
|
print(Fore.GREEN + f'{dev_name}: {trace}: checksum updated' + Style.RESET_ALL)
|
||||||
|
target['traces'][trace][dev_name]['checksum'] = checksum
|
||||||
|
|
||||||
|
with open(traces_file[0], 'w', encoding='utf-8') as target_file:
|
||||||
|
yaml.dump(target, target_file)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> None:
|
||||||
|
"""Parse args"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Tool to generate patch from checksums ",
|
||||||
|
epilog="Example: update_traces_checksum.py --rev $(git rev-parse HEAD) "
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--rev", metavar="revision", help="repository git revision", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--token",
|
||||||
|
metavar="token",
|
||||||
|
help="force GitLab token, otherwise it's read from ~/.config/gitlab-token",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
token = read_token(args.token)
|
||||||
|
|
||||||
|
gl = gitlab.Gitlab(url="https://gitlab.freedesktop.org", private_token=token)
|
||||||
|
|
||||||
|
cur_project = get_gitlab_project(gl, "mesa")
|
||||||
|
|
||||||
|
print(f"Revision: {args.rev}")
|
||||||
|
(pipe, cur_project) = wait_for_pipeline([cur_project], args.rev)
|
||||||
|
print(f"Pipeline: {pipe.web_url}")
|
||||||
|
gather_results(cur_project, pipe)
|
||||||
|
|
||||||
|
sys.exit()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
10
bin/ci/update_traces_checksum.sh
Executable file
10
bin/ci/update_traces_checksum.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
this_dir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")
|
||||||
|
readonly this_dir
|
||||||
|
|
||||||
|
exec \
|
||||||
|
"$this_dir/../python-venv.sh" \
|
||||||
|
"$this_dir/requirements.txt" \
|
||||||
|
"$this_dir/update_traces_checksum.py" "$@"
|
|
@ -45,24 +45,26 @@ def is_commit_valid(commit: str) -> bool:
|
||||||
return ret == 0
|
return ret == 0
|
||||||
|
|
||||||
|
|
||||||
def branch_has_commit(upstream: str, branch: str, commit: str) -> bool:
|
def branch_has_commit(upstream_branch: str, commit: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the commit is actually present in the branch
|
Returns True if the commit is actually present in the branch
|
||||||
"""
|
"""
|
||||||
ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
|
ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
|
||||||
commit, upstream + '/' + branch],
|
commit, upstream_branch],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL)
|
stderr=subprocess.DEVNULL)
|
||||||
return ret == 0
|
return ret == 0
|
||||||
|
|
||||||
|
|
||||||
def branch_has_backport_of_commit(upstream: str, branch: str, commit: str) -> str:
|
def branch_has_backport_of_commit(upstream_branch: str, commit: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the commit hash if the commit has been backported to the branch,
|
Returns the commit hash if the commit has been backported to the branch,
|
||||||
or an empty string if is hasn't
|
or an empty string if is hasn't
|
||||||
"""
|
"""
|
||||||
|
upstream, _ = upstream_branch.split('/', 1)
|
||||||
|
|
||||||
out = subprocess.check_output(['git', 'log', '--format=%H',
|
out = subprocess.check_output(['git', 'log', '--format=%H',
|
||||||
upstream + '..' + upstream + '/' + branch,
|
upstream + '..' + upstream_branch,
|
||||||
'--grep', 'cherry picked from commit ' + commit],
|
'--grep', 'cherry picked from commit ' + commit],
|
||||||
stderr=subprocess.DEVNULL)
|
stderr=subprocess.DEVNULL)
|
||||||
return out.decode().strip()
|
return out.decode().strip()
|
||||||
|
@ -125,17 +127,15 @@ if __name__ == "__main__":
|
||||||
help='colorize output (default: true if stdout is a terminal)')
|
help='colorize output (default: true if stdout is a terminal)')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
upstream, branch = args.branch.split('/', 1)
|
if branch_has_commit(args.branch, args.commit):
|
||||||
|
print_(args, True, 'Commit ' + args.commit + ' is in branch ' + args.branch)
|
||||||
if branch_has_commit(upstream, branch, args.commit):
|
|
||||||
print_(args, True, 'Commit ' + args.commit + ' is in branch ' + branch)
|
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
backport = branch_has_backport_of_commit(upstream, branch, args.commit)
|
backport = branch_has_backport_of_commit(args.branch, args.commit)
|
||||||
if backport:
|
if backport:
|
||||||
print_(args, True,
|
print_(args, True,
|
||||||
'Commit ' + args.commit + ' was backported to branch ' + branch + ' as commit ' + backport)
|
'Commit ' + args.commit + ' was backported to branch ' + args.branch + ' as commit ' + backport)
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
print_(args, False, 'Commit ' + args.commit + ' is NOT in branch ' + branch)
|
print_(args, False, 'Commit ' + args.commit + ' is NOT in branch ' + args.branch)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
|
@ -88,33 +88,31 @@ def test_is_commit_valid(commit: str, expected: bool) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'branch, commit, expected',
|
'branch, commit, expected',
|
||||||
[
|
[
|
||||||
('20.1', '20.1-branchpoint', True),
|
(get_upstream() + '/20.1', '20.1-branchpoint', True),
|
||||||
('20.1', '20.0', False),
|
(get_upstream() + '/20.1', '20.0', False),
|
||||||
('20.1', 'main', False),
|
(get_upstream() + '/20.1', 'main', False),
|
||||||
('20.1', 'e58a10af640ba58b6001f5c5ad750b782547da76', True),
|
(get_upstream() + '/20.1', 'e58a10af640ba58b6001f5c5ad750b782547da76', True),
|
||||||
('20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', True),
|
(get_upstream() + '/20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', True),
|
||||||
('staging/20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', True),
|
(get_upstream() + '/staging/20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', True),
|
||||||
('20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', False),
|
(get_upstream() + '/20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', False),
|
||||||
('main', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', True),
|
(get_upstream() + '/main', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', True),
|
||||||
('20.0', 'd043d24654c851f0be57dbbf48274b5373dea42b', False),
|
(get_upstream() + '/20.0', 'd043d24654c851f0be57dbbf48274b5373dea42b', False),
|
||||||
])
|
])
|
||||||
def test_branch_has_commit(branch: str, commit: str, expected: bool) -> None:
|
def test_branch_has_commit(branch: str, commit: str, expected: bool) -> None:
|
||||||
upstream = get_upstream()
|
assert branch_has_commit(branch, commit) == expected
|
||||||
assert branch_has_commit(upstream, branch, commit) == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'branch, commit, expected',
|
'branch, commit, expected',
|
||||||
[
|
[
|
||||||
('20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', 'd043d24654c851f0be57dbbf48274b5373dea42b'),
|
(get_upstream() + '/20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', 'd043d24654c851f0be57dbbf48274b5373dea42b'),
|
||||||
('staging/20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', 'd043d24654c851f0be57dbbf48274b5373dea42b'),
|
(get_upstream() + '/staging/20.1', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', 'd043d24654c851f0be57dbbf48274b5373dea42b'),
|
||||||
('20.1', '20.1-branchpoint', ''),
|
(get_upstream() + '/20.1', '20.1-branchpoint', ''),
|
||||||
('20.1', '20.0', ''),
|
(get_upstream() + '/20.1', '20.0', ''),
|
||||||
('20.1', '20.2', ''),
|
(get_upstream() + '/20.1', '20.2', 'abac4859618e02aea00f705b841a7c5c5007ad1a'),
|
||||||
('20.1', 'main', ''),
|
(get_upstream() + '/20.1', 'main', ''),
|
||||||
('20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', ''),
|
(get_upstream() + '/20.1', 'd043d24654c851f0be57dbbf48274b5373dea42b', ''),
|
||||||
('20.0', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', ''),
|
(get_upstream() + '/20.0', 'dd2bd68fa69124c86cd008b256d06f44fab8e6cd', '8cd4f57381cefe69019a3282d457d5bda3644030'),
|
||||||
])
|
])
|
||||||
def test_branch_has_backport_of_commit(branch: str, commit: str, expected: bool) -> None:
|
def test_branch_has_backport_of_commit(branch: str, commit: str, expected: bool) -> None:
|
||||||
upstream = get_upstream()
|
assert branch_has_backport_of_commit(branch, commit) == expected
|
||||||
assert branch_has_backport_of_commit(upstream, branch, commit) == expected
|
|
||||||
|
|
|
@ -78,9 +78,9 @@ def commit(message: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _calculate_release_start(major: str, minor: str) -> datetime.date:
|
def _calculate_release_start(major: str, minor: str) -> datetime.date:
|
||||||
"""Calclulate the start of the release for release candidates.
|
"""Calculate the start of the release for release candidates.
|
||||||
|
|
||||||
This is quarterly, on the second wednesday, in Januray, April, July, and Octobor.
|
This is quarterly, on the second wednesday, in January, April, July, and October.
|
||||||
"""
|
"""
|
||||||
quarter = datetime.date.fromisoformat(f'20{major}-0{[1, 4, 7, 10][int(minor)]}-01')
|
quarter = datetime.date.fromisoformat(f'20{major}-0{[1, 4, 7, 10][int(minor)]}-01')
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ def mock_csv(data: typing.List[gen_calendar_entries.CalendarRowType]) -> typing.
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, scope='module')
|
@pytest.fixture(autouse=True, scope='module')
|
||||||
def disable_git_commits() -> None:
|
def disable_git_commits() -> None:
|
||||||
"""Mock out the commit function so no git commits are made durring testing."""
|
"""Mock out the commit function so no git commits are made during testing."""
|
||||||
with mock.patch('bin.gen_calendar_entries.commit', mock.Mock()):
|
with mock.patch('bin.gen_calendar_entries.commit', mock.Mock()):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
|
@ -168,6 +168,7 @@ class Inliner(states.Inliner):
|
||||||
break
|
break
|
||||||
# Quote all original backslashes
|
# Quote all original backslashes
|
||||||
checked = re.sub('\x00', "\\\x00", checked)
|
checked = re.sub('\x00', "\\\x00", checked)
|
||||||
|
checked = re.sub('@', '\\@', checked)
|
||||||
return docutils.utils.unescape(checked, 1)
|
return docutils.utils.unescape(checked, 1)
|
||||||
|
|
||||||
inliner = Inliner();
|
inliner = Inliner();
|
||||||
|
@ -217,7 +218,10 @@ async def parse_issues(commits: str) -> typing.List[str]:
|
||||||
|
|
||||||
async def gather_bugs(version: str) -> typing.List[str]:
|
async def gather_bugs(version: str) -> typing.List[str]:
|
||||||
commits = await gather_commits(version)
|
commits = await gather_commits(version)
|
||||||
issues = await parse_issues(commits)
|
if commits:
|
||||||
|
issues = await parse_issues(commits)
|
||||||
|
else:
|
||||||
|
issues = []
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
async with aiohttp.ClientSession(loop=loop) as session:
|
async with aiohttp.ClientSession(loop=loop) as session:
|
||||||
|
@ -276,7 +280,7 @@ def calculate_next_version(version: str, is_point: bool) -> str:
|
||||||
def calculate_previous_version(version: str, is_point: bool) -> str:
|
def calculate_previous_version(version: str, is_point: bool) -> str:
|
||||||
"""Calculate the previous version to compare to.
|
"""Calculate the previous version to compare to.
|
||||||
|
|
||||||
In the case of -rc to final that verison is the previous .0 release,
|
In the case of -rc to final that version is the previous .0 release,
|
||||||
(19.3.0 in the case of 20.0.0, for example). for point releases that is
|
(19.3.0 in the case of 20.0.0, for example). for point releases that is
|
||||||
the last point release. This value will be the same as the input value
|
the last point release. This value will be the same as the input value
|
||||||
for a point release, but different for a major release.
|
for a point release, but different for a major release.
|
||||||
|
@ -295,7 +299,7 @@ def calculate_previous_version(version: str, is_point: bool) -> str:
|
||||||
|
|
||||||
|
|
||||||
def get_features(is_point_release: bool) -> typing.Generator[str, None, None]:
|
def get_features(is_point_release: bool) -> typing.Generator[str, None, None]:
|
||||||
p = pathlib.Path(__file__).parent.parent / 'docs' / 'relnotes' / 'new_features.txt'
|
p = pathlib.Path('docs') / 'relnotes' / 'new_features.txt'
|
||||||
if p.exists() and p.stat().st_size > 0:
|
if p.exists() and p.stat().st_size > 0:
|
||||||
if is_point_release:
|
if is_point_release:
|
||||||
print("WARNING: new features being introduced in a point release", file=sys.stderr)
|
print("WARNING: new features being introduced in a point release", file=sys.stderr)
|
||||||
|
@ -303,6 +307,7 @@ def get_features(is_point_release: bool) -> typing.Generator[str, None, None]:
|
||||||
for line in f:
|
for line in f:
|
||||||
yield line.rstrip()
|
yield line.rstrip()
|
||||||
p.unlink()
|
p.unlink()
|
||||||
|
subprocess.run(['git', 'add', p])
|
||||||
else:
|
else:
|
||||||
yield "None"
|
yield "None"
|
||||||
|
|
||||||
|
@ -320,12 +325,13 @@ def update_release_notes_index(version: str) -> None:
|
||||||
if first_list and line.startswith('-'):
|
if first_list and line.startswith('-'):
|
||||||
first_list = False
|
first_list = False
|
||||||
new_relnotes.append(f'- :doc:`{version} release notes <relnotes/{version}>`\n')
|
new_relnotes.append(f'- :doc:`{version} release notes <relnotes/{version}>`\n')
|
||||||
if not first_list and second_list and line.startswith(' relnotes/'):
|
if (not first_list and second_list and
|
||||||
|
re.match(r' \d+.\d+(.\d+)? <relnotes/\d+.\d+(.\d+)?>', line)):
|
||||||
second_list = False
|
second_list = False
|
||||||
new_relnotes.append(f' relnotes/{version}\n')
|
new_relnotes.append(f' {version} <relnotes/{version}>\n')
|
||||||
new_relnotes.append(line)
|
new_relnotes.append(line)
|
||||||
|
|
||||||
with relnotes_index_path.open('w') as f:
|
with relnotes_index_path.open('w', encoding='utf-8') as f:
|
||||||
for line in new_relnotes:
|
for line in new_relnotes:
|
||||||
f.write(line)
|
f.write(line)
|
||||||
|
|
||||||
|
@ -333,7 +339,7 @@ def update_release_notes_index(version: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
v = pathlib.Path(__file__).parent.parent / 'VERSION'
|
v = pathlib.Path('VERSION')
|
||||||
with v.open('rt') as f:
|
with v.open('rt') as f:
|
||||||
raw_version = f.read().strip()
|
raw_version = f.read().strip()
|
||||||
is_point_release = '-rc' not in raw_version
|
is_point_release = '-rc' not in raw_version
|
||||||
|
@ -350,8 +356,8 @@ async def main() -> None:
|
||||||
gather_bugs(previous_version),
|
gather_bugs(previous_version),
|
||||||
)
|
)
|
||||||
|
|
||||||
final = pathlib.Path(__file__).parent.parent / 'docs' / 'relnotes' / f'{this_version}.rst'
|
final = pathlib.Path('docs') / 'relnotes' / f'{this_version}.rst'
|
||||||
with final.open('wt') as f:
|
with final.open('wt', encoding='utf-8') as f:
|
||||||
try:
|
try:
|
||||||
f.write(TEMPLATE.render(
|
f.write(TEMPLATE.render(
|
||||||
bugfix=is_point_release,
|
bugfix=is_point_release,
|
||||||
|
@ -368,6 +374,7 @@ async def main() -> None:
|
||||||
))
|
))
|
||||||
except:
|
except:
|
||||||
print(exceptions.text_error_template().render())
|
print(exceptions.text_error_template().render())
|
||||||
|
return
|
||||||
|
|
||||||
subprocess.run(['git', 'add', final])
|
subprocess.run(['git', 'add', final])
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ async def test_gather_commits():
|
||||||
'content, bugs',
|
'content, bugs',
|
||||||
[
|
[
|
||||||
# It is important to have the title on a new line, as
|
# It is important to have the title on a new line, as
|
||||||
# textwrap.dedent wont work otherwise.
|
# textwrap.dedent won't work otherwise.
|
||||||
|
|
||||||
# Test the `Closes: #N` syntax
|
# Test the `Closes: #N` syntax
|
||||||
(
|
(
|
||||||
|
@ -113,7 +113,7 @@ async def test_gather_commits():
|
||||||
'''\
|
'''\
|
||||||
A commit for for something else completely
|
A commit for for something else completely
|
||||||
|
|
||||||
Closes: https://github.com/Organiztion/project/1234
|
Closes: https://github.com/Organization/project/1234
|
||||||
''',
|
''',
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
|
@ -198,3 +198,8 @@ async def test_parse_issues(content: str, bugs: typing.List[str]) -> None:
|
||||||
mock.patch('bin.gen_release_notes.gather_commits', mock.AsyncMock(return_value='sha\n')):
|
mock.patch('bin.gen_release_notes.gather_commits', mock.AsyncMock(return_value='sha\n')):
|
||||||
ids = await parse_issues('1234 not used')
|
ids = await parse_issues('1234 not used')
|
||||||
assert set(ids) == set(bugs)
|
assert set(ids) == set(bugs)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_rst_escape():
|
||||||
|
out = inliner.quoteInline('foo@bar')
|
||||||
|
assert out == 'foo\@bar'
|
||||||
|
|
|
@ -89,8 +89,8 @@ python ./bin/gen_vs_module_defs.py --in_file src/gallium/targets/lavapipe/vulkan
|
||||||
'''
|
'''
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description=gen_help)
|
parser = argparse.ArgumentParser(description=gen_help)
|
||||||
parser.add_argument('--in_file', help='input template moudle definition file')
|
parser.add_argument('--in_file', help='input template module definition file')
|
||||||
parser.add_argument('--out_file', help='output moudle definition file')
|
parser.add_argument('--out_file', help='output module definition file')
|
||||||
parser.add_argument('--compiler_abi', help='compiler abi')
|
parser.add_argument('--compiler_abi', help='compiler abi')
|
||||||
parser.add_argument('--compiler_id', help='compiler id')
|
parser.add_argument('--compiler_id', help='compiler id')
|
||||||
parser.add_argument('--cpu_family', help='cpu family')
|
parser.add_argument('--cpu_family', help='cpu family')
|
||||||
|
|
|
@ -118,35 +118,36 @@ SOURCES = [
|
||||||
'api': 'opencl',
|
'api': 'opencl',
|
||||||
'inc_folder': 'CL',
|
'inc_folder': 'CL',
|
||||||
'sources': [
|
'sources': [
|
||||||
Source('include/CL/opencl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/opencl.h'),
|
Source('include/CL/opencl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/opencl.h'),
|
||||||
Source('include/CL/cl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl.h'),
|
Source('include/CL/cl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl.h'),
|
||||||
Source('include/CL/cl_platform.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_platform.h'),
|
Source('include/CL/cl_platform.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_platform.h'),
|
||||||
Source('include/CL/cl_gl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_gl.h'),
|
Source('include/CL/cl_gl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_gl.h'),
|
||||||
Source('include/CL/cl_gl_ext.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_gl_ext.h'),
|
Source('include/CL/cl_gl_ext.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_gl_ext.h'),
|
||||||
Source('include/CL/cl_ext.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_ext.h'),
|
Source('include/CL/cl_ext.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_ext.h'),
|
||||||
Source('include/CL/cl_version.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_version.h'),
|
Source('include/CL/cl_version.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_version.h'),
|
||||||
Source('include/CL/cl_icd.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_icd.h'),
|
Source('include/CL/cl_icd.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_icd.h'),
|
||||||
Source('include/CL/cl_egl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_egl.h'),
|
Source('include/CL/cl_egl.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_egl.h'),
|
||||||
Source('include/CL/cl_d3d10.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_d3d10.h'),
|
Source('include/CL/cl_d3d10.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_d3d10.h'),
|
||||||
Source('include/CL/cl_d3d11.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_d3d11.h'),
|
Source('include/CL/cl_d3d11.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_d3d11.h'),
|
||||||
Source('include/CL/cl_dx9_media_sharing.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_dx9_media_sharing.h'),
|
Source('include/CL/cl_dx9_media_sharing.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_dx9_media_sharing.h'),
|
||||||
Source('include/CL/cl_dx9_media_sharing_intel.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_dx9_media_sharing_intel.h'),
|
Source('include/CL/cl_dx9_media_sharing_intel.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_dx9_media_sharing_intel.h'),
|
||||||
Source('include/CL/cl_ext_intel.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_ext_intel.h'),
|
Source('include/CL/cl_ext_intel.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_ext_intel.h'),
|
||||||
Source('include/CL/cl_va_api_media_sharing_intel.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/master/CL/cl_va_api_media_sharing_intel.h'),
|
Source('include/CL/cl_va_api_media_sharing_intel.h', 'https://github.com/KhronosGroup/OpenCL-Headers/raw/main/CL/cl_va_api_media_sharing_intel.h'),
|
||||||
|
|
||||||
Source('include/CL/cl.hpp', 'https://github.com/KhronosGroup/OpenCL-CLHPP/raw/master/include/CL/cl.hpp'),
|
Source('include/CL/cl.hpp', 'https://github.com/KhronosGroup/OpenCL-CLHPP/raw/5f3cc41df821a3e5988490232082a3e3b82c0283/include/CL/cl.hpp'),
|
||||||
Source('include/CL/cl2.hpp', 'https://github.com/KhronosGroup/OpenCL-CLHPP/raw/master/include/CL/cl2.hpp'),
|
Source('include/CL/cl2.hpp', 'https://github.com/KhronosGroup/OpenCL-CLHPP/raw/main/include/CL/cl2.hpp'),
|
||||||
|
Source('include/CL/opencl.hpp', 'https://github.com/KhronosGroup/OpenCL-CLHPP/raw/main/include/CL/opencl.hpp'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
'api': 'spirv',
|
'api': 'spirv',
|
||||||
'sources': [
|
'sources': [
|
||||||
Source('src/compiler/spirv/spirv.h', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/master/include/spirv/unified1/spirv.h'),
|
Source('src/compiler/spirv/spirv.h', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/main/include/spirv/unified1/spirv.h'),
|
||||||
Source('src/compiler/spirv/spirv.core.grammar.json', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/master/include/spirv/unified1/spirv.core.grammar.json'),
|
Source('src/compiler/spirv/spirv.core.grammar.json', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/main/include/spirv/unified1/spirv.core.grammar.json'),
|
||||||
Source('src/compiler/spirv/OpenCL.std.h', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/master/include/spirv/unified1/OpenCL.std.h'),
|
Source('src/compiler/spirv/OpenCL.std.h', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/main/include/spirv/unified1/OpenCL.std.h'),
|
||||||
Source('src/compiler/spirv/GLSL.std.450.h', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/master/include/spirv/unified1/GLSL.std.450.h'),
|
Source('src/compiler/spirv/GLSL.std.450.h', 'https://github.com/KhronosGroup/SPIRV-Headers/raw/main/include/spirv/unified1/GLSL.std.450.h'),
|
||||||
Source('src/compiler/spirv/GLSL.ext.AMD.h', 'https://github.com/KhronosGroup/glslang/raw/master/SPIRV/GLSL.ext.AMD.h'), # FIXME: is this the canonical source?
|
Source('src/compiler/spirv/GLSL.ext.AMD.h', 'https://github.com/KhronosGroup/glslang/raw/main/SPIRV/GLSL.ext.AMD.h'), # FIXME: is this the canonical source?
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from os import get_terminal_size
|
|
||||||
from textwrap import wrap
|
|
||||||
from mesonbuild import coredata
|
|
||||||
from mesonbuild import optinterpreter
|
|
||||||
|
|
||||||
(COLUMNS, _) = get_terminal_size()
|
|
||||||
|
|
||||||
def describe_option(option_name: str, option_default_value: str,
|
|
||||||
option_type: str, option_message: str) -> None:
|
|
||||||
print('name: ' + option_name)
|
|
||||||
print('default: ' + option_default_value)
|
|
||||||
print('type: ' + option_type)
|
|
||||||
for line in wrap(option_message, width=COLUMNS - 9):
|
|
||||||
print(' ' + line)
|
|
||||||
print('---')
|
|
||||||
|
|
||||||
oi = optinterpreter.OptionInterpreter('')
|
|
||||||
oi.process('meson_options.txt')
|
|
||||||
|
|
||||||
for (name, value) in oi.options.items():
|
|
||||||
if isinstance(value, coredata.UserStringOption):
|
|
||||||
describe_option(name,
|
|
||||||
value.value,
|
|
||||||
'string',
|
|
||||||
"You can type what you want, but make sure it makes sense")
|
|
||||||
elif isinstance(value, coredata.UserBooleanOption):
|
|
||||||
describe_option(name,
|
|
||||||
'true' if value.value else 'false',
|
|
||||||
'boolean',
|
|
||||||
"You can set it to 'true' or 'false'")
|
|
||||||
elif isinstance(value, coredata.UserIntegerOption):
|
|
||||||
describe_option(name,
|
|
||||||
str(value.value),
|
|
||||||
'integer',
|
|
||||||
"You can set it to any integer value between '{}' and '{}'".format(value.min_value, value.max_value))
|
|
||||||
elif isinstance(value, coredata.UserUmaskOption):
|
|
||||||
describe_option(name,
|
|
||||||
str(value.value),
|
|
||||||
'umask',
|
|
||||||
"You can set it to 'preserve' or a value between '0000' and '0777'")
|
|
||||||
elif isinstance(value, coredata.UserComboOption):
|
|
||||||
choices = '[' + ', '.join(["'" + v + "'" for v in value.choices]) + ']'
|
|
||||||
describe_option(name,
|
|
||||||
value.value,
|
|
||||||
'combo',
|
|
||||||
"You can set it to any one of those values: " + choices)
|
|
||||||
elif isinstance(value, coredata.UserArrayOption):
|
|
||||||
choices = '[' + ', '.join(["'" + v + "'" for v in value.choices]) + ']'
|
|
||||||
value = '[' + ', '.join(["'" + v + "'" for v in value.value]) + ']'
|
|
||||||
describe_option(name,
|
|
||||||
value,
|
|
||||||
'array',
|
|
||||||
"You can set it to one or more of those values: " + choices)
|
|
||||||
elif isinstance(value, coredata.UserFeatureOption):
|
|
||||||
describe_option(name,
|
|
||||||
value.value,
|
|
||||||
'feature',
|
|
||||||
"You can set it to 'auto', 'enabled', or 'disabled'")
|
|
||||||
else:
|
|
||||||
print(name + ' is an option of a type unknown to this script')
|
|
||||||
print('---')
|
|
|
@ -25,7 +25,7 @@
|
||||||
"""Perf annotate for JIT code.
|
"""Perf annotate for JIT code.
|
||||||
|
|
||||||
Linux `perf annotate` does not work with JIT code. This script takes the data
|
Linux `perf annotate` does not work with JIT code. This script takes the data
|
||||||
produced by `perf script` command, plus the diassemblies outputed by gallivm
|
produced by `perf script` command, plus the diassemblies outputted by gallivm
|
||||||
into /tmp/perf-XXXXX.map.asm and produces output similar to `perf annotate`.
|
into /tmp/perf-XXXXX.map.asm and produces output similar to `perf annotate`.
|
||||||
|
|
||||||
See docs/llvmpipe.rst for usage instructions.
|
See docs/llvmpipe.rst for usage instructions.
|
||||||
|
|
|
@ -27,7 +27,7 @@ from pick.ui import UI, PALETTE
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
u = UI()
|
u = UI()
|
||||||
evl = urwid.AsyncioEventLoop(loop=asyncio.get_event_loop())
|
evl = urwid.AsyncioEventLoop(loop=asyncio.new_event_loop())
|
||||||
loop = urwid.MainLoop(u.render(), PALETTE, event_loop=evl, handle_mouse=False)
|
loop = urwid.MainLoop(u.render(), PALETTE, event_loop=evl, handle_mouse=False)
|
||||||
u.mainloop = loop
|
u.mainloop = loop
|
||||||
loop.run()
|
loop.run()
|
||||||
|
|
10
bin/pick-ui.sh
Executable file
10
bin/pick-ui.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
this_dir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")
|
||||||
|
readonly this_dir
|
||||||
|
|
||||||
|
exec \
|
||||||
|
"$this_dir/python-venv.sh" \
|
||||||
|
"$this_dir/pick/requirements.txt" \
|
||||||
|
"$this_dir/pick-ui.py" "$@"
|
|
@ -40,16 +40,19 @@ if typing.TYPE_CHECKING:
|
||||||
sha: str
|
sha: str
|
||||||
description: str
|
description: str
|
||||||
nominated: bool
|
nominated: bool
|
||||||
nomination_type: typing.Optional[int]
|
nomination_type: int
|
||||||
resolution: typing.Optional[int]
|
resolution: typing.Optional[int]
|
||||||
main_sha: typing.Optional[str]
|
main_sha: typing.Optional[str]
|
||||||
because_sha: typing.Optional[str]
|
because_sha: typing.Optional[str]
|
||||||
|
notes: typing.Optional[str] = attr.ib(None)
|
||||||
|
|
||||||
IS_FIX = re.compile(r'^\s*fixes:\s*([a-f0-9]{6,40})', flags=re.MULTILINE | re.IGNORECASE)
|
IS_FIX = re.compile(r'^\s*fixes:\s*([a-f0-9]{6,40})', flags=re.MULTILINE | re.IGNORECASE)
|
||||||
# FIXME: I dislike the duplication in this regex, but I couldn't get it to work otherwise
|
# FIXME: I dislike the duplication in this regex, but I couldn't get it to work otherwise
|
||||||
IS_CC = re.compile(r'^\s*cc:\s*["\']?([0-9]{2}\.[0-9])?["\']?\s*["\']?([0-9]{2}\.[0-9])?["\']?\s*\<?mesa-stable',
|
IS_CC = re.compile(r'^\s*cc:\s*["\']?([0-9]{2}\.[0-9])?["\']?\s*["\']?([0-9]{2}\.[0-9])?["\']?\s*\<?mesa-stable',
|
||||||
flags=re.MULTILINE | re.IGNORECASE)
|
flags=re.MULTILINE | re.IGNORECASE)
|
||||||
IS_REVERT = re.compile(r'This reverts commit ([0-9a-f]{40})')
|
IS_REVERT = re.compile(r'This reverts commit ([0-9a-f]{40})')
|
||||||
|
IS_BACKPORT = re.compile(r'^\s*backport-to:\s*(\d{2}\.\d),?\s*(\d{2}\.\d)?',
|
||||||
|
flags=re.MULTILINE | re.IGNORECASE)
|
||||||
|
|
||||||
# XXX: hack
|
# XXX: hack
|
||||||
SEM = asyncio.Semaphore(50)
|
SEM = asyncio.Semaphore(50)
|
||||||
|
@ -71,6 +74,8 @@ class NominationType(enum.Enum):
|
||||||
CC = 0
|
CC = 0
|
||||||
FIXES = 1
|
FIXES = 1
|
||||||
REVERT = 2
|
REVERT = 2
|
||||||
|
NONE = 3
|
||||||
|
BACKPORT = 4
|
||||||
|
|
||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
|
@ -116,24 +121,24 @@ class Commit:
|
||||||
sha: str = attr.ib()
|
sha: str = attr.ib()
|
||||||
description: str = attr.ib()
|
description: str = attr.ib()
|
||||||
nominated: bool = attr.ib(False)
|
nominated: bool = attr.ib(False)
|
||||||
nomination_type: typing.Optional[NominationType] = attr.ib(None)
|
nomination_type: NominationType = attr.ib(NominationType.NONE)
|
||||||
resolution: Resolution = attr.ib(Resolution.UNRESOLVED)
|
resolution: Resolution = attr.ib(Resolution.UNRESOLVED)
|
||||||
main_sha: typing.Optional[str] = attr.ib(None)
|
main_sha: typing.Optional[str] = attr.ib(None)
|
||||||
because_sha: typing.Optional[str] = attr.ib(None)
|
because_sha: typing.Optional[str] = attr.ib(None)
|
||||||
|
notes: typing.Optional[str] = attr.ib(None)
|
||||||
|
|
||||||
def to_json(self) -> 'CommitDict':
|
def to_json(self) -> 'CommitDict':
|
||||||
d: typing.Dict[str, typing.Any] = attr.asdict(self)
|
d: typing.Dict[str, typing.Any] = attr.asdict(self)
|
||||||
if self.nomination_type is not None:
|
d['nomination_type'] = self.nomination_type.value
|
||||||
d['nomination_type'] = self.nomination_type.value
|
|
||||||
if self.resolution is not None:
|
if self.resolution is not None:
|
||||||
d['resolution'] = self.resolution.value
|
d['resolution'] = self.resolution.value
|
||||||
return typing.cast('CommitDict', d)
|
return typing.cast('CommitDict', d)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, data: 'CommitDict') -> 'Commit':
|
def from_json(cls, data: 'CommitDict') -> 'Commit':
|
||||||
c = cls(data['sha'], data['description'], data['nominated'], main_sha=data['main_sha'], because_sha=data['because_sha'])
|
c = cls(data['sha'], data['description'], data['nominated'], main_sha=data['main_sha'],
|
||||||
if data['nomination_type'] is not None:
|
because_sha=data['because_sha'], notes=data['notes'])
|
||||||
c.nomination_type = NominationType(data['nomination_type'])
|
c.nomination_type = NominationType(data['nomination_type'])
|
||||||
if data['resolution'] is not None:
|
if data['resolution'] is not None:
|
||||||
c.resolution = Resolution(data['resolution'])
|
c.resolution = Resolution(data['resolution'])
|
||||||
return c
|
return c
|
||||||
|
@ -202,6 +207,14 @@ class Commit:
|
||||||
assert v
|
assert v
|
||||||
await ui.feedback(f'{self.sha} ({self.description}) committed successfully')
|
await ui.feedback(f'{self.sha} ({self.description}) committed successfully')
|
||||||
|
|
||||||
|
async def update_notes(self, ui: 'UI', notes: typing.Optional[str]) -> None:
|
||||||
|
self.notes = notes
|
||||||
|
async with ui.git_lock:
|
||||||
|
ui.save()
|
||||||
|
v = await commit_state(message=f'Updates notes for {self.sha}')
|
||||||
|
assert v
|
||||||
|
await ui.feedback(f'{self.sha} ({self.description}) notes updated successfully')
|
||||||
|
|
||||||
|
|
||||||
async def get_new_commits(sha: str) -> typing.List[typing.Tuple[str, str]]:
|
async def get_new_commits(sha: str) -> typing.List[typing.Tuple[str, str]]:
|
||||||
# Try to get the authoritative upstream main
|
# Try to get the authoritative upstream main
|
||||||
|
@ -266,13 +279,11 @@ async def resolve_nomination(commit: 'Commit', version: str) -> 'Commit':
|
||||||
out = _out.decode()
|
out = _out.decode()
|
||||||
|
|
||||||
# We give precedence to fixes and cc tags over revert tags.
|
# We give precedence to fixes and cc tags over revert tags.
|
||||||
# XXX: not having the walrus operator available makes me sad :=
|
if fix_for_commit := IS_FIX.search(out):
|
||||||
m = IS_FIX.search(out)
|
|
||||||
if m:
|
|
||||||
# We set the nomination_type and because_sha here so that we can later
|
# We set the nomination_type and because_sha here so that we can later
|
||||||
# check to see if this fixes another staged commit.
|
# check to see if this fixes another staged commit.
|
||||||
try:
|
try:
|
||||||
commit.because_sha = fixed = await full_sha(m.group(1))
|
commit.because_sha = fixed = await full_sha(fix_for_commit.group(1))
|
||||||
except PickUIException:
|
except PickUIException:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -281,18 +292,22 @@ async def resolve_nomination(commit: 'Commit', version: str) -> 'Commit':
|
||||||
commit.nominated = True
|
commit.nominated = True
|
||||||
return commit
|
return commit
|
||||||
|
|
||||||
m = IS_CC.search(out)
|
if backport_to := IS_BACKPORT.search(out):
|
||||||
if m:
|
if version in backport_to.groups():
|
||||||
if m.groups() == (None, None) or version in m.groups():
|
commit.nominated = True
|
||||||
|
commit.nomination_type = NominationType.BACKPORT
|
||||||
|
return commit
|
||||||
|
|
||||||
|
if cc_to := IS_CC.search(out):
|
||||||
|
if cc_to.groups() == (None, None) or version in cc_to.groups():
|
||||||
commit.nominated = True
|
commit.nominated = True
|
||||||
commit.nomination_type = NominationType.CC
|
commit.nomination_type = NominationType.CC
|
||||||
return commit
|
return commit
|
||||||
|
|
||||||
m = IS_REVERT.search(out)
|
if revert_of := IS_REVERT.search(out):
|
||||||
if m:
|
|
||||||
# See comment for IS_FIX path
|
# See comment for IS_FIX path
|
||||||
try:
|
try:
|
||||||
commit.because_sha = reverted = await full_sha(m.group(1))
|
commit.because_sha = reverted = await full_sha(revert_of.group(1))
|
||||||
except PickUIException:
|
except PickUIException:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -94,9 +94,9 @@ class TestRE:
|
||||||
Reviewed-by: Jonathan Marek <jonathan@marek.ca>
|
Reviewed-by: Jonathan Marek <jonathan@marek.ca>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_FIX.search(message)
|
fix_for_commit = core.IS_FIX.search(message)
|
||||||
assert m is not None
|
assert fix_for_commit is not None
|
||||||
assert m.group(1) == '3d09bb390a39'
|
assert fix_for_commit.group(1) == '3d09bb390a39'
|
||||||
|
|
||||||
class TestCC:
|
class TestCC:
|
||||||
|
|
||||||
|
@ -114,9 +114,9 @@ class TestRE:
|
||||||
Reviewed-by: Bas Nieuwenhuizen <bas@basnieuwenhuizen.nl>
|
Reviewed-by: Bas Nieuwenhuizen <bas@basnieuwenhuizen.nl>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_CC.search(message)
|
cc_to = core.IS_CC.search(message)
|
||||||
assert m is not None
|
assert cc_to is not None
|
||||||
assert m.group(1) == '19.2'
|
assert cc_to.group(1) == '19.2'
|
||||||
|
|
||||||
def test_multiple_branches(self):
|
def test_multiple_branches(self):
|
||||||
"""Tests commit with more than one branch specified"""
|
"""Tests commit with more than one branch specified"""
|
||||||
|
@ -130,10 +130,10 @@ class TestRE:
|
||||||
Reviewed-by: Pierre-Eric Pelloux-Prayer <pierre-eric.pelloux-prayer@amd.com>
|
Reviewed-by: Pierre-Eric Pelloux-Prayer <pierre-eric.pelloux-prayer@amd.com>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_CC.search(message)
|
cc_to = core.IS_CC.search(message)
|
||||||
assert m is not None
|
assert cc_to is not None
|
||||||
assert m.group(1) == '19.1'
|
assert cc_to.group(1) == '19.1'
|
||||||
assert m.group(2) == '19.2'
|
assert cc_to.group(2) == '19.2'
|
||||||
|
|
||||||
def test_no_branch(self):
|
def test_no_branch(self):
|
||||||
"""Tests commit with no branch specification"""
|
"""Tests commit with no branch specification"""
|
||||||
|
@ -148,8 +148,8 @@ class TestRE:
|
||||||
Reviewed-by: Lionel Landwerlin <lionel.g.landwerlin@intel.com>
|
Reviewed-by: Lionel Landwerlin <lionel.g.landwerlin@intel.com>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_CC.search(message)
|
cc_to = core.IS_CC.search(message)
|
||||||
assert m is not None
|
assert cc_to is not None
|
||||||
|
|
||||||
def test_quotes(self):
|
def test_quotes(self):
|
||||||
"""Tests commit with quotes around the versions"""
|
"""Tests commit with quotes around the versions"""
|
||||||
|
@ -162,9 +162,9 @@ class TestRE:
|
||||||
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_CC.search(message)
|
cc_to = core.IS_CC.search(message)
|
||||||
assert m is not None
|
assert cc_to is not None
|
||||||
assert m.group(1) == '20.0'
|
assert cc_to.group(1) == '20.0'
|
||||||
|
|
||||||
def test_multiple_quotes(self):
|
def test_multiple_quotes(self):
|
||||||
"""Tests commit with quotes around the versions"""
|
"""Tests commit with quotes around the versions"""
|
||||||
|
@ -177,10 +177,10 @@ class TestRE:
|
||||||
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_CC.search(message)
|
cc_to = core.IS_CC.search(message)
|
||||||
assert m is not None
|
assert cc_to is not None
|
||||||
assert m.group(1) == '20.0'
|
assert cc_to.group(1) == '20.0'
|
||||||
assert m.group(2) == '20.1'
|
assert cc_to.group(2) == '20.1'
|
||||||
|
|
||||||
def test_single_quotes(self):
|
def test_single_quotes(self):
|
||||||
"""Tests commit with quotes around the versions"""
|
"""Tests commit with quotes around the versions"""
|
||||||
|
@ -193,9 +193,9 @@ class TestRE:
|
||||||
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_CC.search(message)
|
cc_to = core.IS_CC.search(message)
|
||||||
assert m is not None
|
assert cc_to is not None
|
||||||
assert m.group(1) == '20.0'
|
assert cc_to.group(1) == '20.0'
|
||||||
|
|
||||||
def test_multiple_single_quotes(self):
|
def test_multiple_single_quotes(self):
|
||||||
"""Tests commit with quotes around the versions"""
|
"""Tests commit with quotes around the versions"""
|
||||||
|
@ -208,10 +208,10 @@ class TestRE:
|
||||||
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/3454>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_CC.search(message)
|
cc_to = core.IS_CC.search(message)
|
||||||
assert m is not None
|
assert cc_to is not None
|
||||||
assert m.group(1) == '20.0'
|
assert cc_to.group(1) == '20.0'
|
||||||
assert m.group(2) == '20.1'
|
assert cc_to.group(2) == '20.1'
|
||||||
|
|
||||||
class TestRevert:
|
class TestRevert:
|
||||||
|
|
||||||
|
@ -232,9 +232,61 @@ class TestRE:
|
||||||
Reviewed-by: Bas Nieuwenhuizen <bas@basnieuwenhuizen.nl>
|
Reviewed-by: Bas Nieuwenhuizen <bas@basnieuwenhuizen.nl>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
m = core.IS_REVERT.search(message)
|
revert_of = core.IS_REVERT.search(message)
|
||||||
assert m is not None
|
assert revert_of is not None
|
||||||
assert m.group(1) == '2ca8629fa9b303e24783b76a7b3b0c2513e32fbd'
|
assert revert_of.group(1) == '2ca8629fa9b303e24783b76a7b3b0c2513e32fbd'
|
||||||
|
|
||||||
|
class TestBackportTo:
|
||||||
|
|
||||||
|
def test_single_release(self):
|
||||||
|
"""Tests commit meant for a single branch, ie, 19.1"""
|
||||||
|
message = textwrap.dedent("""\
|
||||||
|
radv: fix DCC fast clear code for intensity formats
|
||||||
|
|
||||||
|
This fixes a rendering issue with DiRT 4 on GFX10. Only GFX10 was
|
||||||
|
affected because intensity formats are different.
|
||||||
|
|
||||||
|
Backport-to: 19.2
|
||||||
|
Closes: https://gitlab.freedesktop.org/mesa/mesa/-/issues/1923
|
||||||
|
Signed-off-by: Samuel Pitoiset <samuel.pitoiset@gmail.com>
|
||||||
|
Reviewed-by: Bas Nieuwenhuizen <bas@basnieuwenhuizen.nl>
|
||||||
|
""")
|
||||||
|
|
||||||
|
backport_to = core.IS_BACKPORT.search(message)
|
||||||
|
assert backport_to is not None
|
||||||
|
assert backport_to.groups() == ('19.2', None)
|
||||||
|
|
||||||
|
def test_multiple_release_space(self):
|
||||||
|
"""Tests commit with more than one branch specified"""
|
||||||
|
message = textwrap.dedent("""\
|
||||||
|
radeonsi: enable zerovram for Rocket League
|
||||||
|
|
||||||
|
Fixes corruption on game startup.
|
||||||
|
Closes: https://gitlab.freedesktop.org/mesa/mesa/-/issues/1888
|
||||||
|
|
||||||
|
Backport-to: 19.1 19.2
|
||||||
|
Reviewed-by: Pierre-Eric Pelloux-Prayer <pierre-eric.pelloux-prayer@amd.com>
|
||||||
|
""")
|
||||||
|
|
||||||
|
backport_to = core.IS_BACKPORT.search(message)
|
||||||
|
assert backport_to is not None
|
||||||
|
assert backport_to.groups() == ('19.1', '19.2')
|
||||||
|
|
||||||
|
def test_multiple_release_comma(self):
|
||||||
|
"""Tests commit with more than one branch specified"""
|
||||||
|
message = textwrap.dedent("""\
|
||||||
|
radeonsi: enable zerovram for Rocket League
|
||||||
|
|
||||||
|
Fixes corruption on game startup.
|
||||||
|
Closes: https://gitlab.freedesktop.org/mesa/mesa/-/issues/1888
|
||||||
|
|
||||||
|
Backport-to: 19.1, 19.2
|
||||||
|
Reviewed-by: Pierre-Eric Pelloux-Prayer <pierre-eric.pelloux-prayer@amd.com>
|
||||||
|
""")
|
||||||
|
|
||||||
|
backport_to = core.IS_BACKPORT.search(message)
|
||||||
|
assert backport_to is not None
|
||||||
|
assert backport_to.groups() == ('19.1', '19.2')
|
||||||
|
|
||||||
|
|
||||||
class TestResolveNomination:
|
class TestResolveNomination:
|
||||||
|
@ -242,7 +294,7 @@ class TestResolveNomination:
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class FakeSubprocess:
|
class FakeSubprocess:
|
||||||
|
|
||||||
"""A fake asyncio.subprocess like classe for use with mock."""
|
"""A fake asyncio.subprocess like class for use with mock."""
|
||||||
|
|
||||||
out: typing.Optional[bytes] = attr.ib(None)
|
out: typing.Optional[bytes] = attr.ib(None)
|
||||||
returncode: int = attr.ib(0)
|
returncode: int = attr.ib(0)
|
||||||
|
@ -323,6 +375,28 @@ class TestResolveNomination:
|
||||||
assert not c.nominated
|
assert not c.nominated
|
||||||
assert c.nomination_type is None
|
assert c.nomination_type is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_backport_is_nominated(self):
|
||||||
|
s = self.FakeSubprocess(b'Backport-to: 16.2')
|
||||||
|
c = core.Commit('abcdef1234567890', 'a commit')
|
||||||
|
|
||||||
|
with mock.patch('bin.pick.core.asyncio.create_subprocess_exec', s.mock):
|
||||||
|
await core.resolve_nomination(c, '16.2')
|
||||||
|
|
||||||
|
assert c.nominated
|
||||||
|
assert c.nomination_type is core.NominationType.BACKPORT
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_backport_is_not_nominated(self):
|
||||||
|
s = self.FakeSubprocess(b'Backport-to: 16.2')
|
||||||
|
c = core.Commit('abcdef1234567890', 'a commit')
|
||||||
|
|
||||||
|
with mock.patch('bin.pick.core.asyncio.create_subprocess_exec', s.mock):
|
||||||
|
await core.resolve_nomination(c, '16.1')
|
||||||
|
|
||||||
|
assert not c.nominated
|
||||||
|
assert c.nomination_type is None
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_revert_is_nominated(self):
|
async def test_revert_is_nominated(self):
|
||||||
s = self.FakeSubprocess(b'This reverts commit 1234567890123456789012345678901234567890.')
|
s = self.FakeSubprocess(b'This reverts commit 1234567890123456789012345678901234567890.')
|
||||||
|
@ -347,6 +421,21 @@ class TestResolveNomination:
|
||||||
assert not c.nominated
|
assert not c.nominated
|
||||||
assert c.nomination_type is core.NominationType.REVERT
|
assert c.nomination_type is core.NominationType.REVERT
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_is_fix_and_backport(self):
|
||||||
|
s = self.FakeSubprocess(
|
||||||
|
b'Fixes: 3d09bb390a39 (etnaviv: GC7000: State changes for HALTI3..5)\n'
|
||||||
|
b'Backport-to: 16.1'
|
||||||
|
)
|
||||||
|
c = core.Commit('abcdef1234567890', 'a commit')
|
||||||
|
|
||||||
|
with mock.patch('bin.pick.core.asyncio.create_subprocess_exec', s.mock):
|
||||||
|
with mock.patch('bin.pick.core.is_commit_in_branch', self.return_true):
|
||||||
|
await core.resolve_nomination(c, '16.1')
|
||||||
|
|
||||||
|
assert c.nominated
|
||||||
|
assert c.nomination_type is core.NominationType.FIXES
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_is_fix_and_cc(self):
|
async def test_is_fix_and_cc(self):
|
||||||
s = self.FakeSubprocess(
|
s = self.FakeSubprocess(
|
||||||
|
|
2
bin/pick/requirements.txt
Normal file
2
bin/pick/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
attrs==23.1.0
|
||||||
|
urwid==2.1.2
|
|
@ -47,6 +47,13 @@ class RootWidget(urwid.Frame):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.ui = ui
|
self.ui = ui
|
||||||
|
|
||||||
|
|
||||||
|
class CommitList(urwid.ListBox):
|
||||||
|
|
||||||
|
def __init__(self, *args, ui: 'UI', **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.ui = ui
|
||||||
|
|
||||||
def keypress(self, size: int, key: str) -> typing.Optional[str]:
|
def keypress(self, size: int, key: str) -> typing.Optional[str]:
|
||||||
if key == 'q':
|
if key == 'q':
|
||||||
raise urwid.ExitMainLoop()
|
raise urwid.ExitMainLoop()
|
||||||
|
@ -101,6 +108,23 @@ class CommitWidget(urwid.Text):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class FocusAwareEdit(urwid.Edit):
|
||||||
|
|
||||||
|
"""An Edit type that signals when it comes into and leaves focus."""
|
||||||
|
|
||||||
|
signals = urwid.Edit.signals + ['focus_changed']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.__is_focus = False
|
||||||
|
|
||||||
|
def render(self, size: typing.Tuple[int], focus: bool = False) -> urwid.Canvas:
|
||||||
|
if focus != self.__is_focus:
|
||||||
|
self._emit("focus_changed", focus)
|
||||||
|
self.__is_focus = focus
|
||||||
|
return super().render(size, focus)
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class UI:
|
class UI:
|
||||||
|
|
||||||
|
@ -112,6 +136,7 @@ class UI:
|
||||||
|
|
||||||
commit_list: typing.List['urwid.Button'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False)
|
commit_list: typing.List['urwid.Button'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False)
|
||||||
feedback_box: typing.List['urwid.Text'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False)
|
feedback_box: typing.List['urwid.Text'] = attr.ib(factory=lambda: urwid.SimpleFocusListWalker([]), init=False)
|
||||||
|
notes: 'FocusAwareEdit' = attr.ib(factory=lambda: FocusAwareEdit('', multiline=True), init=False)
|
||||||
header: 'urwid.Text' = attr.ib(factory=lambda: urwid.Text('Mesa Stable Picker', align='center'), init=False)
|
header: 'urwid.Text' = attr.ib(factory=lambda: urwid.Text('Mesa Stable Picker', align='center'), init=False)
|
||||||
body: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_body(), True), init=False)
|
body: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_body(), True), init=False)
|
||||||
footer: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_footer(), True), init=False)
|
footer: 'urwid.Columns' = attr.ib(attr.Factory(lambda s: s._make_footer(), True), init=False)
|
||||||
|
@ -122,10 +147,36 @@ class UI:
|
||||||
new_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False)
|
new_commits: typing.List['core.Commit'] = attr.ib(factory=list, init=False)
|
||||||
git_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock, init=False)
|
git_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock, init=False)
|
||||||
|
|
||||||
|
def _get_current_commit(self) -> typing.Optional['core.Commit']:
|
||||||
|
entry = self.commit_list.get_focus()[0]
|
||||||
|
return entry.original_widget.commit if entry is not None else None
|
||||||
|
|
||||||
|
def _change_notes_cb(self) -> None:
|
||||||
|
commit = self._get_current_commit()
|
||||||
|
if commit and commit.notes:
|
||||||
|
self.notes.set_edit_text(commit.notes)
|
||||||
|
else:
|
||||||
|
self.notes.set_edit_text('')
|
||||||
|
|
||||||
|
def _change_notes_focus_cb(self, notes: 'FocusAwareEdit', focus: 'bool') -> 'None':
|
||||||
|
# in the case of coming into focus we don't want to do anything
|
||||||
|
if focus:
|
||||||
|
return
|
||||||
|
commit = self._get_current_commit()
|
||||||
|
if commit is None:
|
||||||
|
return
|
||||||
|
text: str = notes.get_edit_text()
|
||||||
|
if text != commit.notes:
|
||||||
|
asyncio.ensure_future(commit.update_notes(self, text))
|
||||||
|
|
||||||
def _make_body(self) -> 'urwid.Columns':
|
def _make_body(self) -> 'urwid.Columns':
|
||||||
commits = urwid.ListBox(self.commit_list)
|
commits = CommitList(self.commit_list, ui=self)
|
||||||
feedback = urwid.ListBox(self.feedback_box)
|
feedback = urwid.ListBox(self.feedback_box)
|
||||||
return urwid.Columns([commits, feedback])
|
urwid.connect_signal(self.commit_list, 'modified', self._change_notes_cb)
|
||||||
|
notes = urwid.Filler(self.notes)
|
||||||
|
urwid.connect_signal(self.notes, 'focus_changed', self._change_notes_focus_cb)
|
||||||
|
|
||||||
|
return urwid.Columns([urwid.LineBox(commits), urwid.Pile([urwid.LineBox(notes), urwid.LineBox(feedback)])])
|
||||||
|
|
||||||
def _make_footer(self) -> 'urwid.Columns':
|
def _make_footer(self) -> 'urwid.Columns':
|
||||||
body = [
|
body = [
|
||||||
|
@ -134,12 +185,12 @@ class UI:
|
||||||
urwid.Text('[C]herry Pick'),
|
urwid.Text('[C]herry Pick'),
|
||||||
urwid.Text('[D]enominate'),
|
urwid.Text('[D]enominate'),
|
||||||
urwid.Text('[B]ackport'),
|
urwid.Text('[B]ackport'),
|
||||||
urwid.Text('[A]pply additional patch')
|
urwid.Text('[A]pply additional patch'),
|
||||||
]
|
]
|
||||||
return urwid.Columns(body)
|
return urwid.Columns(body)
|
||||||
|
|
||||||
def _make_root(self) -> 'RootWidget':
|
def _make_root(self) -> 'RootWidget':
|
||||||
return RootWidget(self.body, self.header, self.footer, 'body', ui=self)
|
return RootWidget(self.body, urwid.LineBox(self.header), urwid.LineBox(self.footer), 'body', ui=self)
|
||||||
|
|
||||||
def render(self) -> 'WidgetType':
|
def render(self) -> 'WidgetType':
|
||||||
asyncio.ensure_future(self.update())
|
asyncio.ensure_future(self.update())
|
||||||
|
|
47
bin/python-venv.sh
Executable file
47
bin/python-venv.sh
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
readonly requirements_file=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
venv_dir="$(dirname "$requirements_file")"/.venv
|
||||||
|
readonly venv_dir
|
||||||
|
readonly venv_req=$venv_dir/requirements.txt
|
||||||
|
readonly venv_python_version=$venv_dir/python-version.txt
|
||||||
|
|
||||||
|
if [ -d "$venv_dir" ]
|
||||||
|
then
|
||||||
|
if [ ! -r "$venv_python_version" ]
|
||||||
|
then
|
||||||
|
echo "Python environment predates Python version checks."
|
||||||
|
echo "It might be invalid and needs to be regenerated."
|
||||||
|
rm -rf "$venv_dir"
|
||||||
|
elif ! cmp --quiet <(python --version) "$venv_python_version"
|
||||||
|
then
|
||||||
|
old=$(cat "$venv_python_version")
|
||||||
|
new=$(python --version)
|
||||||
|
echo "Python version has changed ($old -> $new)."
|
||||||
|
echo "Python environment needs to be regenerated."
|
||||||
|
unset old new
|
||||||
|
rm -rf "$venv_dir"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -r "$venv_dir/bin/activate" ]
|
||||||
|
then
|
||||||
|
echo "Creating Python environment..."
|
||||||
|
python -m venv "$venv_dir"
|
||||||
|
python --version > "$venv_python_version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=1091
|
||||||
|
source "$venv_dir/bin/activate"
|
||||||
|
|
||||||
|
if ! cmp --quiet "$requirements_file" "$venv_req"
|
||||||
|
then
|
||||||
|
echo "$(realpath --relative-to="$PWD" "$requirements_file") has changed, re-installing..."
|
||||||
|
pip --disable-pip-version-check install --requirement "$requirements_file"
|
||||||
|
cp "$requirements_file" "$venv_req"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python "$@"
|
|
@ -7,12 +7,41 @@ import subprocess
|
||||||
|
|
||||||
# This list contains symbols that _might_ be exported for some platforms
|
# This list contains symbols that _might_ be exported for some platforms
|
||||||
PLATFORM_SYMBOLS = [
|
PLATFORM_SYMBOLS = [
|
||||||
|
'_GLOBAL_OFFSET_TABLE_',
|
||||||
'__bss_end__',
|
'__bss_end__',
|
||||||
'__bss_start__',
|
'__bss_start__',
|
||||||
'__bss_start',
|
'__bss_start',
|
||||||
'__cxa_guard_abort',
|
'__cxa_guard_abort',
|
||||||
'__cxa_guard_acquire',
|
'__cxa_guard_acquire',
|
||||||
'__cxa_guard_release',
|
'__cxa_guard_release',
|
||||||
|
'__cxa_allocate_dependent_exception',
|
||||||
|
'__cxa_allocate_exception',
|
||||||
|
'__cxa_begin_catch',
|
||||||
|
'__cxa_call_unexpected',
|
||||||
|
'__cxa_current_exception_type',
|
||||||
|
'__cxa_current_primary_exception',
|
||||||
|
'__cxa_decrement_exception_refcount',
|
||||||
|
'__cxa_deleted_virtual',
|
||||||
|
'__cxa_demangle',
|
||||||
|
'__cxa_end_catch',
|
||||||
|
'__cxa_free_dependent_exception',
|
||||||
|
'__cxa_free_exception',
|
||||||
|
'__cxa_get_exception_ptr',
|
||||||
|
'__cxa_get_globals',
|
||||||
|
'__cxa_get_globals_fast',
|
||||||
|
'__cxa_increment_exception_refcount',
|
||||||
|
'__cxa_new_handler',
|
||||||
|
'__cxa_pure_virtual',
|
||||||
|
'__cxa_rethrow',
|
||||||
|
'__cxa_rethrow_primary_exception',
|
||||||
|
'__cxa_terminate_handler',
|
||||||
|
'__cxa_throw',
|
||||||
|
'__cxa_uncaught_exception',
|
||||||
|
'__cxa_uncaught_exceptions',
|
||||||
|
'__cxa_unexpected_handler',
|
||||||
|
'__dynamic_cast',
|
||||||
|
'__emutls_get_address',
|
||||||
|
'__gxx_personality_v0',
|
||||||
'__end__',
|
'__end__',
|
||||||
'__odr_asan._glapi_Context',
|
'__odr_asan._glapi_Context',
|
||||||
'__odr_asan._glapi_Dispatch',
|
'__odr_asan._glapi_Dispatch',
|
||||||
|
@ -40,7 +69,7 @@ def get_symbols_nm(nm, lib):
|
||||||
if len(fields) == 2 or fields[1] == 'U':
|
if len(fields) == 2 or fields[1] == 'U':
|
||||||
continue
|
continue
|
||||||
symbol_name = fields[0]
|
symbol_name = fields[0]
|
||||||
if platform_name == 'Linux':
|
if platform_name == 'Linux' or platform_name == 'GNU' or platform_name.startswith('GNU/'):
|
||||||
if symbol_name in PLATFORM_SYMBOLS:
|
if symbol_name in PLATFORM_SYMBOLS:
|
||||||
continue
|
continue
|
||||||
elif platform_name == 'Darwin':
|
elif platform_name == 'Darwin':
|
||||||
|
@ -161,7 +190,7 @@ def main():
|
||||||
continue
|
continue
|
||||||
if symbol[:2] == '_Z':
|
if symbol[:2] == '_Z':
|
||||||
# As ajax found out, the compiler intentionally exports symbols
|
# As ajax found out, the compiler intentionally exports symbols
|
||||||
# that we explicitely asked it not to export, and we can't do
|
# that we explicitly asked it not to export, and we can't do
|
||||||
# anything about it:
|
# anything about it:
|
||||||
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=36022#c4
|
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=36022#c4
|
||||||
continue
|
continue
|
||||||
|
|
129
docs/_exts/bootstrap.py
Normal file
129
docs/_exts/bootstrap.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
# BSD 3-Clause License
|
||||||
|
#
|
||||||
|
# Copyright (c) 2018, pandas
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice, this
|
||||||
|
# list of conditions and the following disclaimer.
|
||||||
|
#
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# * Neither the name of the copyright holder nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
# Based on https://github.com/pydata/pydata-sphinx-theme
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
|
||||||
|
import sphinx
|
||||||
|
from sphinx.ext.autosummary import autosummary_table
|
||||||
|
from sphinx.locale import admonitionlabels
|
||||||
|
|
||||||
|
import types
|
||||||
|
|
||||||
|
class BootstrapHTML5TranslatorMixin:
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
super().__init__(*args, **kwds)
|
||||||
|
self.settings.table_style = "table"
|
||||||
|
|
||||||
|
def starttag(self, *args, **kwargs):
|
||||||
|
"""ensure an aria-level is set for any heading role"""
|
||||||
|
if kwargs.get("ROLE") == "heading" and "ARIA-LEVEL" not in kwargs:
|
||||||
|
kwargs["ARIA-LEVEL"] = "2"
|
||||||
|
return super().starttag(*args, **kwargs)
|
||||||
|
|
||||||
|
def visit_admonition(self, node, name: str = '') -> None:
|
||||||
|
admonitionclasses = {
|
||||||
|
'attention': 'alert-primary',
|
||||||
|
'caution': 'alert-secondary',
|
||||||
|
'danger': 'alert-danger',
|
||||||
|
'error': 'alert-danger',
|
||||||
|
'hint': 'alert-secondary',
|
||||||
|
'important': 'alert-primary',
|
||||||
|
'note': 'alert-info',
|
||||||
|
'seealso': 'alert-info',
|
||||||
|
'tip': 'alert-info',
|
||||||
|
'warning': 'alert-warning',
|
||||||
|
}
|
||||||
|
|
||||||
|
self.body.append(self.starttag(
|
||||||
|
node, 'div', CLASS=('alert ' + admonitionclasses[name])))
|
||||||
|
if name:
|
||||||
|
self.body.append(
|
||||||
|
self.starttag(node, 'div', '', CLASS='h5'))
|
||||||
|
self.body.append(str(admonitionlabels[name]))
|
||||||
|
self.body.append('</div>')
|
||||||
|
|
||||||
|
def visit_table(self, node):
|
||||||
|
# init the attributes
|
||||||
|
atts = {}
|
||||||
|
|
||||||
|
self._table_row_indices.append(0)
|
||||||
|
|
||||||
|
# get the classes
|
||||||
|
classes = [cls.strip(" \t\n") for cls in self.settings.table_style.split(",")]
|
||||||
|
|
||||||
|
# we're looking at the 'real_table', which is wrapped by an autosummary
|
||||||
|
if isinstance(node.parent, autosummary_table):
|
||||||
|
classes += ["autosummary"]
|
||||||
|
|
||||||
|
# add the width if set in a style attribute
|
||||||
|
if "width" in node:
|
||||||
|
atts["style"] = f'width: {node["width"]}'
|
||||||
|
|
||||||
|
# add specific class if align is set
|
||||||
|
if "align" in node:
|
||||||
|
classes.append(f'table-{node["align"]}')
|
||||||
|
|
||||||
|
tag = self.starttag(node, "table", CLASS=" ".join(classes), **atts)
|
||||||
|
self.body.append(tag)
|
||||||
|
|
||||||
|
def setup_translators(app):
|
||||||
|
if app.builder.default_translator_class is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not app.registry.translators.items():
|
||||||
|
translator = types.new_class(
|
||||||
|
"BootstrapHTML5Translator",
|
||||||
|
(
|
||||||
|
BootstrapHTML5TranslatorMixin,
|
||||||
|
app.builder.default_translator_class,
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
app.set_translator(app.builder.name, translator, override=True)
|
||||||
|
else:
|
||||||
|
for name, klass in app.registry.translators.items():
|
||||||
|
if app.builder.format != "html":
|
||||||
|
# Skip translators that are not HTML
|
||||||
|
continue
|
||||||
|
|
||||||
|
translator = types.new_class(
|
||||||
|
"BootstrapHTML5Translator",
|
||||||
|
(
|
||||||
|
BootstrapHTML5TranslatorMixin,
|
||||||
|
klass,
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
app.set_translator(name, translator, override=True)
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.connect("builder-inited", setup_translators)
|
|
@ -6,17 +6,8 @@
|
||||||
import docutils.nodes
|
import docutils.nodes
|
||||||
import sphinx.addnodes
|
import sphinx.addnodes
|
||||||
|
|
||||||
def parse_envvar(env, sig, signode):
|
from sphinx.util.nodes import split_explicit_title
|
||||||
envvar, t, default = sig.split(" ", 2)
|
from docutils import nodes, utils
|
||||||
envvar = envvar.strip().upper()
|
|
||||||
t = "Type: %s" % t.strip(" <>").lower()
|
|
||||||
default = "Default: %s" % default.strip(" ()")
|
|
||||||
signode += sphinx.addnodes.desc_name(envvar, envvar)
|
|
||||||
signode += docutils.nodes.Text(' ')
|
|
||||||
signode += sphinx.addnodes.desc_type(t, t)
|
|
||||||
signode += docutils.nodes.Text(', ')
|
|
||||||
signode += sphinx.addnodes.desc_annotation(default, default)
|
|
||||||
return envvar
|
|
||||||
|
|
||||||
def parse_opcode(env, sig, signode):
|
def parse_opcode(env, sig, signode):
|
||||||
opcode, desc = sig.split("-", 1)
|
opcode, desc = sig.split("-", 1)
|
||||||
|
@ -26,8 +17,33 @@ def parse_opcode(env, sig, signode):
|
||||||
signode += sphinx.addnodes.desc_annotation(desc, desc)
|
signode += sphinx.addnodes.desc_annotation(desc, desc)
|
||||||
return opcode
|
return opcode
|
||||||
|
|
||||||
|
|
||||||
|
def ext_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||||
|
text = utils.unescape(text)
|
||||||
|
has_explicit_title, title, ext = split_explicit_title(text)
|
||||||
|
|
||||||
|
parts = ext.split('_', 2)
|
||||||
|
if parts[0] == 'VK':
|
||||||
|
full_url = f'https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/{ext}.html'
|
||||||
|
elif parts[0] == 'GL':
|
||||||
|
full_url = f'https://registry.khronos.org/OpenGL/extensions/{parts[1]}/{parts[1]}_{parts[2]}.txt'
|
||||||
|
else:
|
||||||
|
raise Exception(f'Unexpected API: {parts[0]}')
|
||||||
|
|
||||||
|
pnode = nodes.reference(title, title, internal=False, refuri=full_url)
|
||||||
|
return [pnode], []
|
||||||
|
|
||||||
|
def vkfeat_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||||
|
text = utils.unescape(text)
|
||||||
|
has_explicit_title, title, ext = split_explicit_title(text)
|
||||||
|
|
||||||
|
full_url = f'https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#features-{ext}'
|
||||||
|
|
||||||
|
pnode = nodes.reference(title, title, internal=False, refuri=full_url)
|
||||||
|
return [pnode], []
|
||||||
|
|
||||||
def setup(app):
|
def setup(app):
|
||||||
app.add_object_type("envvar", "envvar", "%s (environment variable)",
|
|
||||||
parse_envvar)
|
|
||||||
app.add_object_type("opcode", "opcode", "%s (TGSI opcode)",
|
app.add_object_type("opcode", "opcode", "%s (TGSI opcode)",
|
||||||
parse_opcode)
|
parse_opcode)
|
||||||
|
app.add_role('ext', ext_role)
|
||||||
|
app.add_role('vk-feat', vkfeat_role)
|
||||||
|
|
|
@ -40,7 +40,7 @@ import nir_opcodes
|
||||||
OP_DESC_TEMPLATE = mako.template.Template("""
|
OP_DESC_TEMPLATE = mako.template.Template("""
|
||||||
<%
|
<%
|
||||||
def src_decl_list(num_srcs):
|
def src_decl_list(num_srcs):
|
||||||
return ', '.join('nir_ssa_def *src' + str(i) for i in range(num_srcs))
|
return ', '.join('nir_def *src' + str(i) for i in range(num_srcs))
|
||||||
|
|
||||||
def to_yn(b):
|
def to_yn(b):
|
||||||
return 'Y' if b else 'N'
|
return 'Y' if b else 'N'
|
||||||
|
@ -58,6 +58,8 @@ def to_yn(b):
|
||||||
- ${to_yn('associative' in op.algebraic_properties)}
|
- ${to_yn('associative' in op.algebraic_properties)}
|
||||||
- ${to_yn('2src_commutative' in op.algebraic_properties)}
|
- ${to_yn('2src_commutative' in op.algebraic_properties)}
|
||||||
|
|
||||||
|
${("**Description:** " + op.description) if op.description != "" else ""}
|
||||||
|
|
||||||
**Constant-folding:**
|
**Constant-folding:**
|
||||||
|
|
||||||
.. code-block:: c
|
.. code-block:: c
|
||||||
|
@ -66,7 +68,7 @@ ${textwrap.indent(op.const_expr, ' ')}
|
||||||
|
|
||||||
**Builder function:**
|
**Builder function:**
|
||||||
|
|
||||||
.. c:function:: nir_ssa_def *nir_${op.name}(nir_builder *, ${src_decl_list(op.num_inputs)})
|
.. c:function:: nir_def *nir_${op.name}(nir_builder *, ${src_decl_list(op.num_inputs)})
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def parse_rst(state, parent, rst):
|
def parse_rst(state, parent, rst):
|
||||||
|
|
|
@ -81,7 +81,7 @@ Additions to Chapter 8 of the GLES 3.2 Specification (Textures and Samplers)
|
||||||
BGRA_EXT B, G, R, A Color
|
BGRA_EXT B, G, R, A Color
|
||||||
|
|
||||||
|
|
||||||
Add to table 8.9 (Effective internal format correspondig to
|
Add to table 8.9 (Effective internal format corresponding to
|
||||||
external format).
|
external format).
|
||||||
|
|
||||||
Format Type Effective
|
Format Type Effective
|
|
@ -360,7 +360,7 @@ Revision History
|
||||||
Version 4, 2013/02/01 - Add issue #12 regarding texture / renderbuffer
|
Version 4, 2013/02/01 - Add issue #12 regarding texture / renderbuffer
|
||||||
format queries.
|
format queries.
|
||||||
|
|
||||||
Version 5, 2013/02/14 - Add issues #13 and #14 regarding simpler queires
|
Version 5, 2013/02/14 - Add issues #13 and #14 regarding simpler queries
|
||||||
after the context is created and made current.
|
after the context is created and made current.
|
||||||
Add issue #15 regarding the string query.
|
Add issue #15 regarding the string query.
|
||||||
Add issue #16 regarding the value type returned
|
Add issue #16 regarding the value type returned
|
105
docs/_static/specs/MESA_sampler_objects.spec
vendored
Normal file
105
docs/_static/specs/MESA_sampler_objects.spec
vendored
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
Name
|
||||||
|
|
||||||
|
MESA_sampler_objects
|
||||||
|
|
||||||
|
Name Strings
|
||||||
|
|
||||||
|
GL_MESA_sampler_objects
|
||||||
|
|
||||||
|
Contact
|
||||||
|
|
||||||
|
Adam Jackson <ajax@redhat.com>
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
|
||||||
|
Emma Anholt
|
||||||
|
The contributors to ARB_sampler_objects and OpenGL ES 3
|
||||||
|
|
||||||
|
Status
|
||||||
|
|
||||||
|
Shipping
|
||||||
|
|
||||||
|
Version
|
||||||
|
|
||||||
|
Last Modified Date: 14 Sep 2021
|
||||||
|
Author Revision: 3
|
||||||
|
|
||||||
|
Number
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
|
||||||
|
OpenGL ES 2.0 is required.
|
||||||
|
|
||||||
|
This extension interacts with:
|
||||||
|
- EXT_shadow_samplers
|
||||||
|
- EXT_texture_filter_anisotropic
|
||||||
|
- EXT_texture_sRGB_decode
|
||||||
|
- OES_texture_border_clamp
|
||||||
|
|
||||||
|
Overview
|
||||||
|
|
||||||
|
This extension makes the sampler object subset of OpenGL ES 3.0 available
|
||||||
|
in OpenGL ES 2.0 contexts. As the intent is to allow access to the API
|
||||||
|
without necessarily requiring additional renderer functionality, some
|
||||||
|
sampler state that would be mandatory in GLES 3 is dependent on the
|
||||||
|
presence of additional extensions. Under GLES 3.0 or above this extension's
|
||||||
|
name string may be exposed for compatibility, but it is otherwise without
|
||||||
|
effect.
|
||||||
|
|
||||||
|
Refer to the OpenGL ES 3.0 specification for API details not covered here.
|
||||||
|
|
||||||
|
New Procedures and Functions
|
||||||
|
|
||||||
|
void glGenSamplers (GLsizei count, GLuint *samplers);
|
||||||
|
void glDeleteSamplers (GLsizei count, const GLuint *samplers);
|
||||||
|
GLboolean glIsSampler (GLuint sampler);
|
||||||
|
void glBindSampler (GLuint unit, GLuint sampler);
|
||||||
|
void glSamplerParameteri (GLuint sampler, GLenum pname, GLint param);
|
||||||
|
void glSamplerParameteriv (GLuint sampler, GLenum pname, const GLint *param);
|
||||||
|
void glSamplerParameterf (GLuint sampler, GLenum pname, GLfloat param);
|
||||||
|
void glSamplerParameterfv (GLuint sampler, GLenum pname, const GLfloat *param);
|
||||||
|
void glGetSamplerParameteriv (GLuint sampler, GLenum pname, GLint *params);
|
||||||
|
void glGetSamplerParameterfv (GLuint sampler, GLenum pname, GLfloat *params);
|
||||||
|
|
||||||
|
Note that these names are exactly as in ES3, with no MESA suffix.
|
||||||
|
|
||||||
|
New Tokens
|
||||||
|
|
||||||
|
SAMPLER_BINDING 0x8919
|
||||||
|
|
||||||
|
Interactions
|
||||||
|
|
||||||
|
If EXT_shadow_samplers is not supported then TEXTURE_COMPARE_MODE and
|
||||||
|
TEXTURE_COMPARE_FUNC will generate INVALID_ENUM.
|
||||||
|
|
||||||
|
If EXT_texture_filter_anisotropic is not supported then
|
||||||
|
TEXTURE_MAX_ANISOTROPY_EXT will generate INVALID_ENUM.
|
||||||
|
|
||||||
|
If EXT_texture_sRGB_decode is not supported then TEXTURE_SRGB_DECODE_EXT
|
||||||
|
will generate INVALID_ENUM.
|
||||||
|
|
||||||
|
If OES_texture_border_clamp is not supported then TEXTURE_BORDER_COLOR
|
||||||
|
will generate INVALID_ENUM.
|
||||||
|
|
||||||
|
Issues
|
||||||
|
|
||||||
|
1) Why bother?
|
||||||
|
|
||||||
|
Sampler objects, at least in Mesa, are generically supported without any
|
||||||
|
driver-dependent requirements, so enabling this is essentially free. This
|
||||||
|
simplifies application support for otherwise GLES2 hardware, and for
|
||||||
|
drivers in development that haven't yet achieved GLES3.
|
||||||
|
|
||||||
|
Revision History
|
||||||
|
|
||||||
|
Rev. Date Author Changes
|
||||||
|
---- -------- -------- ---------------------------------------------
|
||||||
|
1 2019/10/22 ajax Initial revision
|
||||||
|
2 2019/11/14 ajax Add extension interactions:
|
||||||
|
- EXT_shadow_samplers
|
||||||
|
- EXT_texture_filter_anisotropic
|
||||||
|
- EXT_texture_sRGB_decode
|
||||||
|
- OES_texture_border_clamp
|
||||||
|
3 2021/09/14 ajax Expand the justification and ES3 interaction
|
|
@ -46,7 +46,7 @@ Overview
|
||||||
|
|
||||||
GL_ARB_gpu_shader5 extends GLSL in a number of useful ways. Much of this
|
GL_ARB_gpu_shader5 extends GLSL in a number of useful ways. Much of this
|
||||||
added functionality requires significant hardware support. There are many
|
added functionality requires significant hardware support. There are many
|
||||||
aspects, however, that can be easily implmented on any GPU with "real"
|
aspects, however, that can be easily implemented on any GPU with "real"
|
||||||
integer support (as opposed to simulating integers using floating point
|
integer support (as opposed to simulating integers using floating point
|
||||||
calculations).
|
calculations).
|
||||||
|
|
83
docs/_static/specs/MESA_texture_const_bandwidth.spec
vendored
Normal file
83
docs/_static/specs/MESA_texture_const_bandwidth.spec
vendored
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
Name
|
||||||
|
|
||||||
|
MESA_texture_const_bandwidth
|
||||||
|
|
||||||
|
Name Strings
|
||||||
|
|
||||||
|
GL_MESA_texture_const_bandwidth
|
||||||
|
|
||||||
|
Contact
|
||||||
|
|
||||||
|
Rob Clark <robdclark@chromium.org>
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
|
||||||
|
Rob Clark, Google
|
||||||
|
Lina Versace, Google
|
||||||
|
Tapani Pälli, Intel
|
||||||
|
|
||||||
|
Status
|
||||||
|
|
||||||
|
Proposal
|
||||||
|
|
||||||
|
Version
|
||||||
|
|
||||||
|
Version 1, September, 2023
|
||||||
|
|
||||||
|
Number
|
||||||
|
|
||||||
|
tbd
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
|
||||||
|
Requires EXT_memory_object.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
|
||||||
|
The use of data dependent bandwidth compressed formats (UBWC, AFBC, etc)
|
||||||
|
can introduce a form of side-channel, in that the bandwidth used for
|
||||||
|
texture access is dependent on the texture's contents. In some cases
|
||||||
|
an application may want to disable the use of data dependent formats on
|
||||||
|
specific textures.
|
||||||
|
|
||||||
|
For that purpose, this extension extends EXT_memory_object to introduce
|
||||||
|
a new <param> CONST_BW_TILING_MESA.
|
||||||
|
|
||||||
|
IP Status
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
Issues
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
New Procedures and Functions
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
New Types
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
New Tokens
|
||||||
|
|
||||||
|
Returned in the <params> parameter of GetInternalFormativ or
|
||||||
|
GetInternalFormati64v when the <pname> parameter is TILING_TYPES_EXT,
|
||||||
|
returned in the <params> parameter of GetTexParameter{if}v,
|
||||||
|
GetTexParameterI{i ui}v, GetTextureParameter{if}v, and
|
||||||
|
GetTextureParameterI{i ui}v when the <pname> parameter is
|
||||||
|
TEXTURE_TILING_EXT, and accepted by the <params> parameter of
|
||||||
|
TexParameter{ifx}{v}, TexParameterI{i ui}v, TextureParameter{if}{v},
|
||||||
|
TextureParameterI{i ui}v when the <pname> parameter is
|
||||||
|
TEXTURE_TILING_EXT:
|
||||||
|
|
||||||
|
CONST_BW_TILING_MESA 0x8BBE
|
||||||
|
|
||||||
|
Errors
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
Revision History
|
||||||
|
|
||||||
|
Version 1, 2023-9-28 (Rob Clark)
|
||||||
|
Initial draft.
|
|
@ -51,7 +51,7 @@ Overview
|
||||||
monitor. The screen surface can be scrolled by changing this origin.
|
monitor. The screen surface can be scrolled by changing this origin.
|
||||||
|
|
||||||
This extension also defines functions for controlling the monitor's
|
This extension also defines functions for controlling the monitor's
|
||||||
display mode (width, height, refresh rate, etc), and specifing which
|
display mode (width, height, refresh rate, etc), and specifying which
|
||||||
screen surface is to be displayed on a monitor.
|
screen surface is to be displayed on a monitor.
|
||||||
|
|
||||||
The new EGLModeMESA type and related functions are very similar to the
|
The new EGLModeMESA type and related functions are very similar to the
|
|
@ -12,7 +12,7 @@ Contact
|
||||||
|
|
||||||
Status
|
Status
|
||||||
|
|
||||||
Not shipping.
|
Obsolete.
|
||||||
|
|
||||||
Version
|
Version
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ Changes to Chapter 2 of the GLX 1.3 Specification (Functions and Errors)
|
||||||
In addition, an indirect rendering context can be current for
|
In addition, an indirect rendering context can be current for
|
||||||
only one thread at a time. A direct rendering context may be
|
only one thread at a time. A direct rendering context may be
|
||||||
current to multiple threads, with synchronization of access to
|
current to multiple threads, with synchronization of access to
|
||||||
the context thruogh the GL managed by the application through
|
the context through the GL managed by the application through
|
||||||
mutexes.
|
mutexes.
|
||||||
|
|
||||||
Changes to Chapter 3 of the GLX 1.3 Specification (Functions and Errors)
|
Changes to Chapter 3 of the GLX 1.3 Specification (Functions and Errors)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue