Repository: CMB-S4/spt3g_software (language: python)

Issue:
G3Reader fails on multiple files when last file is empty 
Create empty file:
```
In [3]: wr = core.G3Writer('empty.g3')

In [4]: del wr
```
Create non-empty file:
```
In [5]: fr = core.G3Frame()

In [6]: fr['A'] = 0

In [7]: wr = core.G3Writer('full.g3')

In [8]: wr(fr)
Out[8]: [<spt3g.core.G3Frame at 0x7fd5034ac450>]

In [9]: del wr
```
Read empty file only:
```
In [10]: p = core.G3Pipeline()

In [11]: p.Add(core.G3Reader, filename = ['empty.g3'])

In [12]: p.Run()  #runs fine, no errors
```
Read full file first and then empty file:
```
In [13]: p = core.G3Pipeline()

In [14]: p.Add(core.G3Reader, filename = ['full.g3', 'empty.g3'])

In [15]: p.Run()
ERROR (G3Reader) 18-Jan-2024:11:11:52 CST: Exception raised while reading file empty.g3 (G3Reader.cxx:98 in virtual void G3Reader::Process(G3FramePtr, std::deque<boost::shared_ptr<G3Frame> >&))
WARN (Unknown) 18-Jan-2024:11:11:52 CST: Exception in module "spt3g.core.G3Reader" (N6cereal9ExceptionE): Failed to read 1 bytes from input stream! Read 0 (G3Pipeline.cxx:119 in size_t {anonymous}::PushFrameThroughQueue(G3FramePtr, bool, bool, rusage&, std::vector<{anonymous}::G3Pipeline_mod_data>&, std::vector<{anonymous}::G3Pipeline_mod_data>::iterator, int&, std::deque<{anonymous}::G3Pipeline_proc_data>&, G3FramePtr&))
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-15-ecb398338890> in <module>
----> 1 p.Run()

RuntimeError: Failed to read 1 bytes from input stream! Read 0

```

Note: other combinations (read full file only, read empty file first and full file second) also work fine. 


Discussion / hints:

Fix patch:
diff --git a/core/include/core/G3Reader.h b/core/include/core/G3Reader.h
index 57288829..df554be6 100644
--- a/core/include/core/G3Reader.h
+++ b/core/include/core/G3Reader.h
@@ -26,6 +26,7 @@ class G3Reader : public G3Module {
 	boost::iostreams::filtering_istream stream_;
 	int n_frames_to_read_;
 	int n_frames_read_;
+	int n_frames_cur_;
 	float timeout_;
 	bool track_filename_;
 
diff --git a/core/src/G3Reader.cxx b/core/src/G3Reader.cxx
index db0fceec..77f81754 100644
--- a/core/src/G3Reader.cxx
+++ b/core/src/G3Reader.cxx
@@ -7,7 +7,8 @@
 G3Reader::G3Reader(std::string filename, int n_frames_to_read,
     float timeout, bool track_filename) :
     prefix_file_(false), n_frames_to_read_(n_frames_to_read),
-    n_frames_read_(0), timeout_(timeout), track_filename_(track_filename)
+    n_frames_read_(0), n_frames_cur_(0), timeout_(timeout),
+    track_filename_(track_filename)
 {
 	boost::filesystem::path fpath(filename);
 	if (filename.find("://") == std::string::npos &&
@@ -20,7 +21,8 @@ G3Reader::G3Reader(std::string filename, int n_frames_to_read,
 G3Reader::G3Reader(std::vector<std::string> filename, int n_frames_to_read,
     float timeout, bool track_filename) :
     prefix_file_(false), n_frames_to_read_(n_frames_to_read),
-    n_frames_read_(0), timeout_(timeout), track_filename_(track_filename)
+    n_frames_read_(0), n_frames_cur_(0), timeout_(timeout),
+    track_filename_(track_filename)
 {
 	if (filename.size() == 0)
 		log_fatal("Empty file list provided to G3Reader");
@@ -41,6 +43,7 @@ void G3Reader::StartFile(std::string path)
 {
 	log_info("Starting file %s\n", path.c_str());
 	cur_file_ = path;
+	n_frames_cur_ = 0;
 	(void) g3_istream_from_path(stream_, path, timeout_);
 }
 
@@ -80,7 +83,9 @@ void G3Reader::Process(G3FramePtr frame, std::deque<G3FramePtr> &out)
 	if (Py_IsInitialized())
 		_save = PyEval_SaveThread();
 
-	if (stream_.peek() == EOF) {
+	while (stream_.peek() == EOF) {
+		if (n_frames_cur_ == 0)
+			log_error("Empty file %s", cur_file_.c_str());
 		if (filename_.size() > 0) {
 			StartFile(filename_.front());
 			filename_.pop_front();
@@ -109,6 +114,7 @@ void G3Reader::Process(G3FramePtr frame, std::deque<G3FramePtr> &out)
 	out.push_back(frame);
 
 	n_frames_read_++;
+	n_frames_cur_++;
 }
 
 off_t G3Reader::Seek(off_t offset) {


Test patch:
diff --git a/core/tests/fileio.py b/core/tests/fileio.py
index 0b36de3f..c8b073dc 100755
--- a/core/tests/fileio.py
+++ b/core/tests/fileio.py
@@ -49,6 +49,17 @@ def checkinfo(fr):
 
 assert n == 10, 'Wrong number of frames read (%d should be %d)' % (n, 10)
 
+# Skip empty files
+wr = core.G3Writer("empty.g3")
+del wr
+n = 0
+pipe = core.G3Pipeline()
+pipe.Add(core.G3Reader, filename=["empty.g3", "test.g3", "empty.g3"], track_filename=True)
+pipe.Add(checkinfo)
+pipe.Run()
+
+assert n == 10, 'Wrong number of frames read (%d should be %d)' % (n, 10)
+
 # Indexing
 class CachingReader:
     def __init__(self, filename='test.g3'):