This is an automated email from the ASF dual-hosted git repository.

Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 89fb1dcfedf [Python] Add UnboundedSource ValidatesRunner test (#38892)
89fb1dcfedf is described below

commit 89fb1dcfedfdb9728af3b803d2d812f3b5cd543c
Author: Elia Liu <[email protected]>
AuthorDate: Thu Jul 9 01:18:40 2026 +1000

    [Python] Add UnboundedSource ValidatesRunner test (#38892)
---
 ...PostCommit_Python_ValidatesRunner_Dataflow.json |   3 +-
 ...am_PostCommit_Python_ValidatesRunner_Flink.json |   3 +-
 ...am_PostCommit_Python_ValidatesRunner_Spark.json |   3 +-
 sdks/python/apache_beam/io/iobase_test.py          |   8 +-
 sdks/python/apache_beam/io/unbounded_source.py     | 140 +++++++++++++++++++++
 .../python/apache_beam/io/unbounded_source_test.py | 136 +-------------------
 .../portability/fn_api_runner/fn_runner_test.py    |  29 +++++
 .../runners/portability/spark_runner_test.py       |   4 +
 8 files changed, 187 insertions(+), 139 deletions(-)

diff --git 
a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json 
b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json
index f1ba03a243e..462412f7bd3 100644
--- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json
+++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Dataflow.json
@@ -1,4 +1,5 @@
 {
   "comment": "Modify this file in a trivial way to cause this test suite to 
run",
-  "modification": 5
+  "modification": 5,
+  "https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR 
test"
 }
diff --git 
a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json 
b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json
index b08e2bb8115..962795fcb0a 100644
--- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json
+++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Flink.json
@@ -1,5 +1,6 @@
 {
   "https://github.com/apache/beam/pull/32648": "testing addition of Flink 1.19 
support",
   "https://github.com/apache/beam/pull/34830": "testing",
-  "trigger-2026-04-04": "portable_runner expand_sdf opt-in"
+  "trigger-2026-04-04": "portable_runner expand_sdf opt-in",
+  "https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR 
test"
 }
diff --git 
a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json 
b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json
index bd7b284625b..f4288649d8a 100644
--- a/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json
+++ b/.github/trigger_files/beam_PostCommit_Python_ValidatesRunner_Spark.json
@@ -1,5 +1,6 @@
 {
   "https://github.com/apache/beam/pull/34830": "testing",
   "https://github.com/apache/beam/issues/35429": "testing",
-  "trigger-2026-04-04": "portable_runner expand_sdf opt-in"
+  "trigger-2026-04-04": "portable_runner expand_sdf opt-in",
+  "https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR 
test"
 }
diff --git a/sdks/python/apache_beam/io/iobase_test.py 
b/sdks/python/apache_beam/io/iobase_test.py
index dbedf4681f4..60c26156315 100644
--- a/sdks/python/apache_beam/io/iobase_test.py
+++ b/sdks/python/apache_beam/io/iobase_test.py
@@ -224,22 +224,22 @@ class UseSdfBoundedSourcesTests(unittest.TestCase):
 class UseSdfUnboundedSourcesTests(unittest.TestCase):
   """Covers the UnboundedSource branch in
   ``iobase.Read.expand()``. Uses ``UnboundedCountingSource`` from
-  ``unbounded_source_test`` as a finite fake source (no network).
+  ``unbounded_source`` as a finite fake source (no network).
   """
   def test_read_end_to_end_unbounded(self):
-    from apache_beam.io.unbounded_source_test import UnboundedCountingSource
+    from apache_beam.io.unbounded_source import UnboundedCountingSource
     with beam.Pipeline() as p:
       out = p | beam.io.Read(UnboundedCountingSource(5))
       assert_that(out, equal_to([0, 1, 2, 3, 4]))
 
   def test_read_unbounded_pcollection_is_unbounded(self):
-    from apache_beam.io.unbounded_source_test import UnboundedCountingSource
+    from apache_beam.io.unbounded_source import UnboundedCountingSource
     p = beam.Pipeline()
     out = p | beam.io.Read(UnboundedCountingSource(3))
     self.assertFalse(out.is_bounded)
 
   def test_read_unbounded_serializes_as_expanded_composite(self):
-    from apache_beam.io.unbounded_source_test import UnboundedCountingSource
+    from apache_beam.io.unbounded_source import UnboundedCountingSource
     p = beam.Pipeline()
     p | 'ReadIt' >> beam.io.Read(UnboundedCountingSource(3))
 
diff --git a/sdks/python/apache_beam/io/unbounded_source.py 
b/sdks/python/apache_beam/io/unbounded_source.py
index 3246602252e..ac5f17ce45a 100644
--- a/sdks/python/apache_beam/io/unbounded_source.py
+++ b/sdks/python/apache_beam/io/unbounded_source.py
@@ -93,6 +93,8 @@ from typing import Callable
 from typing import Iterable
 from typing import Optional
 
+from typing_extensions import override
+
 from apache_beam import coders
 from apache_beam.coders.coders import BooleanCoder
 from apache_beam.coders.coders import Coder
@@ -994,3 +996,141 @@ class ReadFromUnboundedSource(PTransform):
 
   def _infer_output_coder(self, input_type=None, input_coder=None):
     return self._source.default_output_coder()
+
+
+# 
------------------------------------------------------------------------------
+# Internal in-memory counting source, shared by Beam's own tests and examples.
+# 
------------------------------------------------------------------------------
+
+# Realistic event-time base away from the Unix epoch.
+_EVENT_TIME_BASE = Timestamp(1729987200)  # 2024-10-27T00:00:00Z
+
+
+class _CountingCheckpointMark(CheckpointMark):
+  def __init__(self, last_index, finalize_log=None):
+    self.last_index = last_index
+    self._finalize_log = finalize_log
+
+  @override
+  def finalize_checkpoint(self):
+    if self._finalize_log is not None:
+      self._finalize_log.append(self.last_index)
+
+  def __eq__(self, other):
+    return (
+        isinstance(other, _CountingCheckpointMark) and
+        other.last_index == self.last_index)
+
+  def __hash__(self):
+    return hash(self.last_index)
+
+  def __repr__(self):
+    return '_CountingCheckpointMark(last_index=%r)' % (self.last_index, )
+
+
+class _CountingReader(UnboundedReader):
+  def __init__(
+      self, count, start_index, finalize_log=None, modulus=1, residue=0):
+    self._count = count
+    self._next = start_index
+    self._modulus = modulus
+    self._residue = residue
+    self._current = None
+    self._exhausted = False
+    self._finalize_log = finalize_log
+    self.closed = False
+
+  def _read_next(self):
+    while self._next < self._count:
+      index = self._next
+      self._next += 1
+      if index % self._modulus == self._residue:
+        self._current = index
+        return True
+    self._exhausted = True
+    return False
+
+  @override
+  def start(self):
+    return self._read_next()
+
+  @override
+  def advance(self):
+    return self._read_next()
+
+  @override
+  def get_current(self):
+    return self._current
+
+  @override
+  def get_current_timestamp(self):
+    return _EVENT_TIME_BASE + self._current
+
+  @override
+  def get_watermark(self):
+    if self._exhausted:
+      return MAX_TIMESTAMP
+    if self._current is None:
+      return MIN_TIMESTAMP
+    return _EVENT_TIME_BASE + self._current
+
+  @override
+  def get_checkpoint_mark(self):
+    last = self._current if self._current is not None else self._next - 1
+    return _CountingCheckpointMark(last, finalize_log=self._finalize_log)
+
+  @override
+  def close(self):
+    self.closed = True
+
+
+class UnboundedCountingSource(UnboundedSource):
+  """In-memory counting source for Beam's own tests and examples.
+
+  Internal: not part of the public ``UnboundedSource`` API. Emits integers
+  ``0..count-1`` with event time ``_EVENT_TIME_BASE + index``, self-terminates
+  at EOF, resumes from ``last_index + 1``, and optionally splits into
+  independent even/odd sub-sources.
+  """
+  def __init__(
+      self,
+      count,
+      finalize_log=None,
+      is_splittable=False,
+      modulus=1,
+      residue=0):
+    self._count = count
+    self._finalize_log = finalize_log
+    self._is_splittable = is_splittable
+    self._modulus = modulus
+    self._residue = residue
+    self.last_reader = None
+
+  @override
+  def split(self, desired_num_splits, options=None):
+    if not self._is_splittable or desired_num_splits < 2:
+      return [self]
+    # Split into independent even/odd sub-sources (each non-splittable).
+    return [
+        UnboundedCountingSource(
+            self._count,
+            finalize_log=self._finalize_log,
+            modulus=2,
+            residue=residue) for residue in (0, 1)
+    ]
+
+  @override
+  def create_reader(self, options, checkpoint_mark):
+    start_index = (
+        0 if checkpoint_mark is None else checkpoint_mark.last_index + 1)
+    self.last_reader = _CountingReader(
+        self._count,
+        start_index,
+        finalize_log=self._finalize_log,
+        modulus=self._modulus,
+        residue=self._residue)
+    return self.last_reader
+
+  @override
+  def get_checkpoint_mark_coder(self):
+    return coders.PickleCoder()
diff --git a/sdks/python/apache_beam/io/unbounded_source_test.py 
b/sdks/python/apache_beam/io/unbounded_source_test.py
index 3b63d0d2de9..6c6651cd0db 100644
--- a/sdks/python/apache_beam/io/unbounded_source_test.py
+++ b/sdks/python/apache_beam/io/unbounded_source_test.py
@@ -29,11 +29,15 @@ from typing_extensions import override
 import apache_beam as beam
 from apache_beam import coders
 from apache_beam.io import unbounded_source as _unbounded_source_module
+from apache_beam.io.unbounded_source import _EVENT_TIME_BASE
 from apache_beam.io.unbounded_source import _NO_DATA
 from apache_beam.io.unbounded_source import CheckpointMark
 from apache_beam.io.unbounded_source import ReadFromUnboundedSource
+from apache_beam.io.unbounded_source import UnboundedCountingSource
 from apache_beam.io.unbounded_source import UnboundedReader
 from apache_beam.io.unbounded_source import UnboundedSource
+from apache_beam.io.unbounded_source import _CountingCheckpointMark
+from apache_beam.io.unbounded_source import _CountingReader
 from apache_beam.io.unbounded_source import _FinalizeCheckpointOnce
 from apache_beam.io.unbounded_source import _ReaderCache
 from apache_beam.io.unbounded_source import _ReadFromUnboundedSourceDoFn
@@ -55,138 +59,6 @@ from apache_beam.utils.timestamp import Timestamp
 
 # pylint: disable=expression-not-assigned
 
-# Realistic event-time base away from the Unix epoch.
-_EVENT_TIME_BASE = Timestamp(1729987200)  # 2024-10-27T00:00:00Z
-
-# 
------------------------------------------------------------------------------
-# In-memory demo source emitting integers 0..count-1 with event time
-# ``_EVENT_TIME_BASE + index``. It self-terminates at EOF, resumes from
-# ``last_index + 1``, and splits into even/odd sub-sources when configured.
-# 
------------------------------------------------------------------------------
-
-
-class _CountingCheckpointMark(CheckpointMark):
-  def __init__(self, last_index, finalize_log=None):
-    self.last_index = last_index
-    self._finalize_log = finalize_log
-
-  @override
-  def finalize_checkpoint(self):
-    if self._finalize_log is not None:
-      self._finalize_log.append(self.last_index)
-
-  def __eq__(self, other):
-    return (
-        isinstance(other, _CountingCheckpointMark) and
-        other.last_index == self.last_index)
-
-  def __hash__(self):
-    return hash(self.last_index)
-
-  def __repr__(self):
-    return '_CountingCheckpointMark(last_index=%r)' % (self.last_index, )
-
-
-class _CountingReader(UnboundedReader):
-  def __init__(
-      self, count, start_index, finalize_log=None, modulus=1, residue=0):
-    self._count = count
-    self._next = start_index
-    self._modulus = modulus
-    self._residue = residue
-    self._current = None
-    self._exhausted = False
-    self._finalize_log = finalize_log
-    self.closed = False
-
-  def _read_next(self):
-    while self._next < self._count:
-      index = self._next
-      self._next += 1
-      if index % self._modulus == self._residue:
-        self._current = index
-        return True
-    self._exhausted = True
-    return False
-
-  @override
-  def start(self):
-    return self._read_next()
-
-  @override
-  def advance(self):
-    return self._read_next()
-
-  @override
-  def get_current(self):
-    return self._current
-
-  @override
-  def get_current_timestamp(self):
-    return _EVENT_TIME_BASE + self._current
-
-  @override
-  def get_watermark(self):
-    if self._exhausted:
-      return MAX_TIMESTAMP
-    if self._current is None:
-      return MIN_TIMESTAMP
-    return _EVENT_TIME_BASE + self._current
-
-  @override
-  def get_checkpoint_mark(self):
-    last = self._current if self._current is not None else self._next - 1
-    return _CountingCheckpointMark(last, finalize_log=self._finalize_log)
-
-  @override
-  def close(self):
-    self.closed = True
-
-
-class UnboundedCountingSource(UnboundedSource):
-  def __init__(
-      self,
-      count,
-      finalize_log=None,
-      is_splittable=False,
-      modulus=1,
-      residue=0):
-    self._count = count
-    self._finalize_log = finalize_log
-    self._is_splittable = is_splittable
-    self._modulus = modulus
-    self._residue = residue
-    self.last_reader = None
-
-  @override
-  def split(self, desired_num_splits, options=None):
-    if not self._is_splittable or desired_num_splits < 2:
-      return [self]
-    # Split into independent even/odd sub-sources (each non-splittable).
-    return [
-        UnboundedCountingSource(
-            self._count,
-            finalize_log=self._finalize_log,
-            modulus=2,
-            residue=residue) for residue in (0, 1)
-    ]
-
-  @override
-  def create_reader(self, options, checkpoint_mark):
-    start_index = (
-        0 if checkpoint_mark is None else checkpoint_mark.last_index + 1)
-    self.last_reader = _CountingReader(
-        self._count,
-        start_index,
-        finalize_log=self._finalize_log,
-        modulus=self._modulus,
-        residue=self._residue)
-    return self.last_reader
-
-  @override
-  def get_checkpoint_mark_coder(self):
-    return coders.PickleCoder()
-
 
 class _StringCountingReader(_CountingReader):
   @override
diff --git 
a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py 
b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py
index 72a5ed4412c..884dd342310 100644
--- 
a/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py
+++ 
b/sdks/python/apache_beam/runners/portability/fn_api_runner/fn_runner_test.py
@@ -46,6 +46,7 @@ import apache_beam as beam
 from apache_beam.coders import coders
 from apache_beam.coders.coders import StrUtf8Coder
 from apache_beam.io import restriction_trackers
+from apache_beam.io.unbounded_source import UnboundedCountingSource
 from apache_beam.io.watermark_estimators import ManualWatermarkEstimator
 from apache_beam.metrics import monitoring_infos
 from apache_beam.metrics.execution import MetricKey
@@ -1170,6 +1171,25 @@ class FnApiRunnerTest(unittest.TestCase):
       actual = (p | beam.Create([10]) | beam.ParDo(SimpleSDF()))
       assert_that(actual, equal_to(range(5)))
 
+  def test_unbounded_source_read(self):
+    """Reads a self-terminating UnboundedSource end to end.
+
+    Exercises the runner-API round trip and the EOF ``MAX_TIMESTAMP`` watermark
+    propagation that lets the downstream window fire.
+    """
+    with self.create_pipeline() as p:
+      out = p | beam.io.Read(UnboundedCountingSource(5))
+      self.assertFalse(out.is_bounded)
+      assert_that(out, equal_to([0, 1, 2, 3, 4]), label='assert_elements')
+      windowed = (
+          out
+          | beam.WindowInto(window.FixedWindows(100))
+          | beam.Map(lambda v: ('all', v))
+          | beam.GroupByKey()
+          | beam.MapTuple(lambda _key, values: sorted(values)))
+      assert_that(
+          windowed, equal_to([[0, 1, 2, 3, 4]]), label='assert_windowed')
+
   def test_group_by_key(self):
     with self.create_pipeline() as p:
       res = (
@@ -2038,6 +2058,9 @@ class FnApiRunnerTestWithMultiWorkers(FnApiRunnerTest):
   def test_sdf_with_watermark_tracking(self):
     raise unittest.SkipTest("This test is for a single worker only.")
 
+  def test_unbounded_source_read(self):
+    raise unittest.SkipTest("This test is for a single worker only.")
+
   def test_sdf_with_dofn_as_watermark_estimator(self):
     raise unittest.SkipTest("This test is for a single worker only.")
 
@@ -2071,6 +2094,9 @@ class 
FnApiRunnerTestWithGrpcAndMultiWorkers(FnApiRunnerTest):
   def test_sdf_with_watermark_tracking(self):
     raise unittest.SkipTest("This test is for a single worker only.")
 
+  def test_unbounded_source_read(self):
+    raise unittest.SkipTest("This test is for a single worker only.")
+
   def test_sdf_with_dofn_as_watermark_estimator(self):
     raise unittest.SkipTest("This test is for a single worker only.")
 
@@ -2115,6 +2141,9 @@ class 
FnApiRunnerTestWithBundleRepeatAndMultiWorkers(FnApiRunnerTest):
   def test_sdf_with_watermark_tracking(self):
     raise unittest.SkipTest("This test is for a single worker only.")
 
+  def test_unbounded_source_read(self):
+    raise unittest.SkipTest("This test is for a single worker only.")
+
   def test_sdf_with_dofn_as_watermark_estimator(self):
     raise unittest.SkipTest("This test is for a single worker only.")
 
diff --git a/sdks/python/apache_beam/runners/portability/spark_runner_test.py 
b/sdks/python/apache_beam/runners/portability/spark_runner_test.py
index fe7160d4e18..54238bbf192 100644
--- a/sdks/python/apache_beam/runners/portability/spark_runner_test.py
+++ b/sdks/python/apache_beam/runners/portability/spark_runner_test.py
@@ -146,6 +146,10 @@ class 
SparkRunnerTest(portable_runner_test.PortableRunnerTest):
     # Skip until Spark runner supports SDF.
     raise unittest.SkipTest("https://github.com/apache/beam/issues/19468";)
 
+  def test_unbounded_source_read(self):
+    # Skip until Spark runner supports SDF.
+    raise unittest.SkipTest("https://github.com/apache/beam/issues/19468";)
+
   def test_sdf_with_watermark_tracking(self):
     # Skip until Spark runner supports SDF.
     raise unittest.SkipTest("https://github.com/apache/beam/issues/19468";)

Reply via email to