Skip to content

Commit

Permalink
docs: Prototype docs examples doubling as tests (AcademySoftwareFound…
Browse files Browse the repository at this point in the history
…ation#3977)

New theory of code examples in the docs: every one should live in the
testsuite somewhere, and merely be included by reference in the docs.
This ensures that every code example works, stays current with the
evolution of the APIs, and never breaks. It also beefs up our test cases
for more thorough code overage in the testsuite.

I've done this with just two examples here, but hopefully it serves as
an example for others to -- bit by bit -- convert all the other code
examples in the docs into tests in a similar manner.

Doing this for any one example is a perfect "good first issue," since
it's bite sized and can easily be done in a day, it doesn't require any
knowledge of deep OIIO internals, and in the process it also teaches you
something about how both the docs and the testsuite are set up.

A few notes:

* I'm structuring it (for now?) as two testsuite entry for each chapter
of the documentation (one for C++ and one for Python).

* Note how I can have many code snippets in a test source file, and use
the Sphinx `:start-after:` and `:end-before:` modifiers of
`literalinclude` to clip particular subsections of the file, using
special comment markers in the code.

While I was there, I also fixed some typos I noticed in the text of the
chapter, oops.

---------

Signed-off-by: Larry Gritz <lg@larrygritz.com>
  • Loading branch information
lgritz authored Sep 12, 2023
1 parent d795a3b commit 4a11f24
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ jobs:
OIIO_CMAKE_FLAGS="-DSANITIZE=address,undefined -DUSE_PYTHON=0"
CMAKE_BUILD_TYPE=Debug
CTEST_TEST_TIMEOUT=1200
CTEST_EXCLUSIONS="broken|cmake-consumer|png-damaged"
CTEST_EXCLUSIONS="broken|png-damaged"
- desc: gcc11/C++17 py3.10 boost1.80 exr3.1 ocio2.2
nametag: linux-vfx2023
os: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ macro (oiio_add_all_tests)
# Freestanding tests:
oiio_add_tests (
cmake-consumer
docs-examples-cpp
iinfo igrep
nonwhole-tiles
oiiotool
Expand Down Expand Up @@ -208,6 +209,7 @@ macro (oiio_add_all_tests)
# libraries to run correctly.
if (USE_PYTHON AND NOT BUILD_OIIOUTIL_ONLY AND NOT SANITIZE)
oiio_add_tests (
docs-examples-python
python-colorconfig
python-deep
python-imagebuf
Expand Down
110 changes: 40 additions & 70 deletions src/doc/imageoutput.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,19 @@ to a file:

.. tabs::

.. code-tab:: c++

#include <OpenImageIO/imageio.h>
using namespace OIIO;
...

const char *filename = "foo.jpg";
const int xres = 640, yres = 480;
const int channels = 3; // RGB
unsigned char pixels[xres * yres * channels];
.. tab:: C++
.. literalinclude:: ../../testsuite/docs-examples-cpp/src/docs-examples-imageoutput.cpp
:language: c++
:start-after: BEGIN-imageoutput-simple
:end-before: END-imageoutput-simple

std::unique_ptr<ImageOutput> out = ImageOutput::create (filename);
if (! out)
return;
ImageSpec spec (xres, yres, channels, TypeDesc::UINT8);
out->open (filename, spec);
out->write_image (TypeDesc::UINT8, pixels);
out->close ();

.. code-tab:: py
.. tab:: Python

import OpenImageIO as oiio
import numpy as np
.. literalinclude:: ../../testsuite/docs-examples-python/src/docs-examples-imageoutput.py
:language: py
:start-after: BEGIN-imageoutput-simple
:end-before: END-imageoutput-simple

filename = "foo.jpg"
xres = 640
yres = 480
channels = 3 # RGB
pixels = np.zeros((yres, xres, channels), dtype=np.uint8)

out = oiio.ImageOutput.create (filename)
if out:
spec = oiio.ImageSpec(xres, yres, channels, 'uint8')
out.open (filename, spec)
out.write_image (pixels)
out.close ()

This little bit of code does a surprising amount of useful work:

Expand All @@ -73,7 +50,7 @@ This little bit of code does a surprising amount of useful work:
out = ImageOutput.create (filename)

* Open the file, write the correct headers, and in all other important ways
prepare a file with the given dimensions (640 x 480), number of color
prepare a file with the given dimensions (320 x 240), number of color
channels (3), and data format (unsigned 8-bit integer).

.. tabs::
Expand Down Expand Up @@ -172,33 +149,26 @@ time, one tile at a time, or by individual rectangles.
Writing individual scanlines
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Individual scanlines may be written using the ``writescanline()`` API call:
Individual scanlines may be written using the ``write_scanline()`` API call:

.. tabs::

.. code-tab:: c++

unsigned char scanline[xres*channels];
out->open (filename, spec);
int z = 0; // Always zero for 2D images
for (int y = 0; y < yres; ++y) {
... generate data in scanline[0..xres*channels-1] ...
out->write_scanline (y, z, TypeDesc::UINT8, scanline);
}
out->close ();
.. tab:: C++
.. literalinclude:: ../../testsuite/docs-examples-cpp/src/docs-examples-imageoutput.cpp
:language: c++
:start-after: BEGIN-imageoutput-scanlines
:end-before: END-imageoutput-scanlines
:dedent: 4

.. code-tab:: py
.. tab:: Python

out.open (filename, spec)
z = 0 # Always zero for 2D images
for y in range(yres) :
# generate data in scanline[0..xres*channels-1] ...
scanline = ...
out.write_scanline (y, z, scanline)
}
out.close ()
.. literalinclude:: ../../testsuite/docs-examples-python/src/docs-examples-imageoutput.py
:language: py
:start-after: BEGIN-imageoutput-scanlines
:end-before: END-imageoutput-scanlines
:dedent: 8

The first two arguments to ``writescanline()`` specify which scanline is
The first two arguments to ``write_scanline()`` specify which scanline is
being written by its vertical (*y*) scanline number (beginning with 0)
and, for volume images, its slice (*z*) number (the slice number should
be 0 for 2D non-volume images). This is followed by a `TypeDesc`
Expand All @@ -212,15 +182,15 @@ All ``ImageOutput`` implementations will accept scanlines in strict order
any). See Section :ref:`sec-imageoutput-random-access-pixels` for details
on out-of-order or repeated scanlines.

The full description of the ``writescanline()`` function may be found
The full description of the ``write_scanline()`` function may be found
in Section :ref:`sec-imageoutput-class-reference`.

Writing individual tiles
^^^^^^^^^^^^^^^^^^^^^^^^

Not all image formats (and therefore not all ``ImageOutput``
implementations) support tiled images. If the format does not support
tiles, then ``writetile()`` will fail. An application using OpenImageIO
tiles, then ``write_tile()`` will fail. An application using OpenImageIO
should gracefully handle the case that tiled output is not available for
the chosen format.

Expand Down Expand Up @@ -297,7 +267,7 @@ individual plugin.
out.write_tile (x, y, z, tile)
out.close ()

The first three arguments to ``writetile()`` specify which tile is being
The first three arguments to ``write_tile()`` specify which tile is being
written by the pixel coordinates of any pixel contained in the tile: *x*
(column), *y* (scanline), and *z* (slice, which should always be 0 for 2D
non-volume images). This is followed by a `TypeDesc` describing the data
Expand All @@ -311,7 +281,7 @@ All ``ImageOutput`` implementations that support tiles will accept tiles in
strict order of increasing *y* rows, and within each row, increasing *x*
column, without missing any tiles. See

The full description of the ``writetile()`` function may be found
The full description of the ``write_tile()`` function may be found
in Section :ref:`sec-imageoutput-class-reference`.

Writing arbitrary rectangles
Expand Down Expand Up @@ -451,15 +421,15 @@ do not attempt to remap values, and do not clamp (except to their full
floating-point range).


It is not required that the pixel data passed to ``writeimage()``,
``writescanline()``, ``writetile()``, or ``write_rectangle()`` actually be
It is not required that the pixel data passed to ``write_image()``,
``write_scanline()``, ``write_tile()``, or ``write_rectangle()`` actually be
in the same data type as that requested as the native pixel data type of the
file. You can fully mix and match data you pass to the various "write"
routines and OpenImageIO will automatically convert from the internal format
to the native file format. For example, the following code will open a TIFF
file that stores pixel data as 16-bit unsigned integers (values ranging from
0 to 65535), compute internal pixel values as floating-point values, with
``writeimage()`` performing the conversion automatically:
``write_image()`` performing the conversion automatically:

.. tabs::

Expand All @@ -484,7 +454,7 @@ file that stores pixel data as 16-bit unsigned integers (values ranging from
out.write_image (pixels)


Note that ``writescanline()``, ``writetile()``, and ``write_rectangle()``
Note that ``write_scanline()``, ``write_tile()``, and ``write_rectangle()``
have a parameter that works in a corresponding manner.


Expand All @@ -507,13 +477,13 @@ passed to the "write" functions are *contiguous*, that is:
* for 3D volumetric images, the first pixel of slice *z* immediately
follows the last pixel of of slice ``z-1``.

Please note that this implies that data passed to ``writetile()`` be
Please note that this implies that data passed to ``write_tile()`` be
contiguous in the shape of a single tile (not just an offset into a whole
image worth of pixels), and that data passed to ``write_rectangle()`` be
contiguous in the dimensions of the rectangle.

The ``writescanline()`` function takes an optional ``xstride`` argument, and
the ``writeimage()``, ``writetile()``, and ``write_rectangle()`` functions
The ``write_scanline()`` function takes an optional ``xstride`` argument, and
the ``write_image()``, ``write_tile()``, and ``write_rectangle()`` functions
take optional ``xstride``, ``ystride``, and ``zstride`` values that describe
the distance, in *bytes*, between successive pixel columns, rows, and
slices, respectively, of the data you are passing. For any of these values
Expand Down Expand Up @@ -1152,8 +1122,8 @@ specification of the subimages at the time you first open the file.
out.write_image (pixels[s])
out.close ()

In both of these examples, we have used ``writeimage()``, but of course
``writescanline()``, ``writetile()``, and ``write_rectangle()`` work as you
In both of these examples, we have used ``write_image()``, but of course
``write_scanline()``, ``write_tile()``, and ``write_rectangle()`` work as you
would expect, on the current subimage.


Expand Down Expand Up @@ -1278,8 +1248,8 @@ used for texture mapping):
out.close ()


In this example, we have used ``writeimage()``, but of course
``writescanline()``, ``writetile()``, and ``write_rectangle()`` work as you
In this example, we have used ``write_image()``, but of course
``write_scanline()``, ``write_tile()``, and ``write_rectangle()`` work as you
would expect, on the current MIP level.


Expand Down
4 changes: 2 additions & 2 deletions src/include/OpenImageIO/texture.h
Original file line number Diff line number Diff line change
Expand Up @@ -1769,8 +1769,8 @@ class OIIO_API TextureSystem {
/// `filenames` will be sized to `ntiles * nvtiles` and filled with the
/// the names of the concrete files comprising the atlas, with an empty
/// ustring corresponding to any unpopulated tiles (the UDIM set is
/// allowed to be sparse). The filename list is indexed as `utile + vtile
/// * nvtiles`.
/// allowed to be sparse). The filename list is indexed as
/// `utile + vtile * nvtiles`.
///
/// This method was added in OpenImageIO 2.3.
virtual void inventory_udim(ustring udimpattern,
Expand Down
10 changes: 8 additions & 2 deletions testsuite/cmake-consumer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
# https://github.com/OpenImageIO/oiio

cmake_minimum_required (VERSION 3.12)
cmake_minimum_required (VERSION 3.15)
project (consumer
LANGUAGES CXX)

Expand All @@ -12,7 +12,7 @@ endif ()

message (STATUS "Building ${PROJECT_NAME} ${PROJECT_VERSION} - ${CMAKE_BUILD_TYPE}")

# Use C++11
# Use C++14
set (CMAKE_CXX_STANDARD 14 CACHE STRING "C++ standard to prefer (14, 17, etc.)")
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_CXX_EXTENSIONS OFF)
Expand All @@ -21,6 +21,12 @@ set (CMAKE_CXX_EXTENSIONS OFF)
# Make sure we have dependencies we need
find_package (OpenImageIO CONFIG REQUIRED)

# Special for OIIO testsuite when running in sanitize mode
if (DEFINED ENV{SANITIZE})
add_compile_options (-fsanitize=$ENV{SANITIZE})
add_link_options (-fsanitize=$ENV{SANITIZE})
endif()


add_executable(consumer consumer.cpp)
target_link_libraries (consumer
Expand Down
45 changes: 45 additions & 0 deletions testsuite/docs-examples-cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/OpenImageIO/oiio

cmake_minimum_required (VERSION 3.15)
project (oiio-docs-examples
LANGUAGES CXX)

if (NOT CMAKE_BUILD_TYPE)
set (CMAKE_BUILD_TYPE "Release")
endif ()

message (STATUS "Building ${PROJECT_NAME} ${PROJECT_VERSION} - ${CMAKE_BUILD_TYPE}")

# Use C++14
set (CMAKE_CXX_STANDARD 14 CACHE STRING "C++ standard to prefer (14, 17, etc.)")
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_CXX_EXTENSIONS OFF)


# Make sure we have dependencies we need
find_package (OpenImageIO CONFIG REQUIRED)

# Special for OIIO testsuite when running in sanitize mode
if (DEFINED ENV{SANITIZE})
add_compile_options (-fsanitize=$ENV{SANITIZE})
add_link_options (-fsanitize=$ENV{SANITIZE})
endif()

add_executable(docs-examples-imageoutput src/docs-examples-imageoutput.cpp)
target_link_libraries (docs-examples-imageoutput
PRIVATE OpenImageIO::OpenImageIO)

# Chapters we haven't done yet:
# add_executable(docs-imageinput src/docs-examples-imageinput.cpp)
# target_link_libraries (docs-examples-imageinput
# PRIVATE OpenImageIO::OpenImageIO)
#
# add_executable(docs-imagebuf src/docs-examples-imagebuf.cpp)
# target_link_libraries (docs-examples-imagebuf
# PRIVATE OpenImageIO::OpenImageIO)
#
# add_executable(docs-imagebufalgo src/docs-examples-imagebufalgo.cpp)
# target_link_libraries (docs-examples-imagebufalgo
# PRIVATE OpenImageIO::OpenImageIO)
4 changes: 4 additions & 0 deletions testsuite/docs-examples-cpp/ref/out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Comparing "simple.tif" and "ref/simple.tif"
PASS
Comparing "scanlines.tif" and "ref/scanlines.tif"
PASS
Binary file added testsuite/docs-examples-cpp/ref/scanlines.tif
Binary file not shown.
Binary file added testsuite/docs-examples-cpp/ref/simple.tif
Binary file not shown.
17 changes: 17 additions & 0 deletions testsuite/docs-examples-cpp/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python

# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/OpenImageIO/oiio


# command += "echo test_source_dir=" + test_source_dir + " >> build.txt ;"
command += run_app("cmake " + test_source_dir + " -DCMAKE_BUILD_TYPE=Release >> build.txt 2>&1", silent=True)
command += run_app("cmake --build . --config Release >> build.txt 2>&1", silent=True)
if platform.system() == 'Windows' :
command += run_app("Release\\docs-examples-imageoutput")
else :
command += run_app("./docs-examples-imageoutput")

outputs = [ "simple.tif", "scanlines.tif",
"out.txt" ]
Loading

0 comments on commit 4a11f24

Please sign in to comment.