Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION.in
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.30
1.31
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Increase the size of layer.str_range and layer.str_cmd.
--
-- A layer's range was VARCHAR(4000), which a fully enumerated frame list
-- (rather than a compact "start-end" range) can exceed for a job with a
-- few thousand frames, rolling back the job launch. str_cmd is widened for
-- the same reason: a command built from many joined paths can also exceed
-- 4000 characters.

ALTER TABLE layer ALTER COLUMN str_range TYPE text;
ALTER TABLE layer ALTER COLUMN str_cmd TYPE text;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
8 changes: 1 addition & 7 deletions pyoutline/outline/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,13 +760,7 @@ def get_frame_range(self):
if not intersect:
return None

# If normalizing does not change the order of frames, return normalized
normalized = FileSequence.FrameSet(str(intersect))
normalized.normalize()
if list(intersect) == list(normalized):
return str(normalized)

return str(intersect)
return outline.util.compact_frame_range(intersect.getAll())
if rng:
return rng

Expand Down
34 changes: 34 additions & 0 deletions pyoutline/outline/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@


from __future__ import absolute_import
from __future__ import annotations
from __future__ import print_function
from __future__ import division

Expand Down Expand Up @@ -83,6 +84,39 @@ def make_frame_set(frames, normalize=True):
fs.normalize()
return fs

def compact_frame_range(frames: list[int]) -> str:
"""
Convert a list of frame numbers into a compact range string,
grouping consecutive frames into "start-end" spans instead of
listing every frame. This keeps the serialized spec small for
large frame counts; Cuebot stores a layer's range in a
fixed-width database column, and a fully enumerated list of a
few thousand frames can exceed it.

Frames are grouped based on their position in the input list, not
on numeric order, so the original frame order and any duplicates
are preserved when the string is parsed back into a FrameSet.

:type frames: List<int>
:param frames: The frame list to convert to a range string.

:rtype: str
:return: A comma-separated string of frames and frame spans.
"""
if not frames:
return ''

spans = []
start = prev = frames[0]
for frame in frames[1:]:
if frame == prev + 1:
prev = frame
continue
spans.append(str(start) if start == prev else '%d-%d' % (start, prev))
start = prev = frame
spans.append(str(start) if start == prev else '%d-%d' % (start, prev))
return ','.join(spans)

def get_slice(frame_range, frames, items):
"""
Given the full frame range, local frame range, and a array items,
Expand Down
31 changes: 31 additions & 0 deletions pyoutline/tests/backend/test_cue.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,37 @@ def testSerializeShellOutline(self):
self.assertEqual(0, len(list(outlineXml.find('depends'))))


class SerializeFrameRangeTest(unittest.TestCase):

"""Tests that large frame ranges serialize compactly.

Cuebot stores a layer's range in a fixed-width database column, so a
range spanning thousands of frames must not be enumerated frame by
frame in the spec XML.
"""

def setUp(self):
outline.Outline.current = None

def testLargeContiguousRangeIsCompactInSpec(self):
ol = outline.Outline(name='maya_render', frame_range='1001-2301')
layer = outline.Layer('defaultRenderLayer', range='1001-2301')
ol.add_layer(layer)
cleanup_layer = outline.Layer('Cleanup')
ol.add_layer(cleanup_layer)

launcher = outline.cuerun.OutlineLauncher(ol, user=TEST_USER)
outlineXml = ET.fromstring(outline.backend.cue.serialize(launcher))

render_layer = next(
layer_el for layer_el in outlineXml.find('job').find('layers').findall('layer')
if layer_el.get('name') == 'defaultRenderLayer')
range_text = render_layer.find('range').text

self.assertEqual('1001-2301', range_text)
self.assertLess(len(range_text), 4000)


class CoresTest(unittest.TestCase):
def setUp(self):
# Ensure to reset current
Expand Down
26 changes: 23 additions & 3 deletions pyoutline/tests/test_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ def test_layer_range_job_range(self):
self.ol.set_frame_range('1000-2000')
self.ol.get_layer('cmd').set_frame_range('1000-2000')

expectedFrameStr = ','.join([str(i) for i in range(1000, 2001)])
self.assertEqual(expectedFrameStr, self.ol.get_layer('cmd').get_frame_range())
# The intersection of two identical consecutive ranges stays consecutive,
# and is returned in compact form rather than as an enumerated frame list.
self.assertEqual('1000-2000', self.ol.get_layer('cmd').get_frame_range())
self.assertEqual('1000-2000', self.ol.get_frame_range())

def test_intersecting_range(self):
Expand All @@ -146,6 +147,23 @@ def test_intersecting_failure(self):

self.assertFalse(self.ol.get_layer('cmd').get_frame_range())

def test_large_contiguous_range_is_compact(self):
# A layer's range is stored in a fixed-width database column on submission,
# so a large contiguous range must stay compact rather than being enumerated
# frame by frame.
self.ol.set_frame_range('1001-2301')
self.ol.get_layer('cmd').set_frame_range('1001-2301')

self.assertEqual('1001-2301', self.ol.get_layer('cmd').get_frame_range())
self.assertEqual('1001-2301', self.ol.get_frame_range())

def test_non_contiguous_range_is_compacted_by_run(self):
self.ol.set_frame_range('1-3,10,20-22')
self.ol.get_layer('cmd').set_frame_range('1-3,10,20-22')

self.assertEqual('1-3,10,20-22', self.ol.get_layer('cmd').get_frame_range())
self.assertEqual('1-3,10,20-22', self.ol.get_frame_range())


class LayerTest(unittest.TestCase):
"""Tests for outline layer."""
Expand Down Expand Up @@ -284,7 +302,9 @@ def test_get_set_frame_range(self):
"""
self.assertEqual(self.ol.get_frame_range(), self.layer.get_frame_range())
self.layer.set_frame_range('1-10')
self.assertEqual('1,2,3,4,5,6,7,8,9,10', self.layer.get_frame_range())
# The intersection of two identical consecutive ranges stays consecutive,
# and is returned in compact form rather than as an enumerated frame list.
self.assertEqual('1-10', self.layer.get_frame_range())

def test_get_set_chunk_size(self):
"""Test get/set of chunk size."""
Expand Down
50 changes: 50 additions & 0 deletions pyoutline/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python

# Copyright Contributors to the OpenCue Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Tests for the outline.util module.
"""

import unittest

import outline.util


class CompactFrameRangeTests(unittest.TestCase):

"""Tests for outline.util.compact_frame_range."""

def test_empty(self):
self.assertEqual('', outline.util.compact_frame_range([]))

def test_single_frame(self):
self.assertEqual('5', outline.util.compact_frame_range([5]))

def test_contiguous_range(self):
self.assertEqual(
'1001-2301', outline.util.compact_frame_range(list(range(1001, 2302))))

def test_non_contiguous_range(self):
self.assertEqual(
'1-3,10,20-22', outline.util.compact_frame_range([1, 2, 3, 10, 20, 21, 22]))

def test_out_of_order_frames_are_not_regrouped(self):
# Grouping is based on the input's position, not numeric order, so a
# descending or shuffled input is not treated as a run.
self.assertEqual('5,4,3', outline.util.compact_frame_range([5, 4, 3]))

def test_duplicate_frames_are_preserved(self):
self.assertEqual('1-2,1', outline.util.compact_frame_range([1, 2, 1]))
Loading