Sfoglia il codice sorgente

Merge branch 'release/1.10.x'

rdb 7 anni fa
parent
commit
ba993aea0d

+ 17 - 10
README.md

@@ -8,12 +8,12 @@ Panda3D
 Panda3D is a game engine, a framework for 3D rendering and game development for
 Panda3D is a game engine, a framework for 3D rendering and game development for
 Python and C++ programs.  Panda3D is open-source and free for any purpose,
 Python and C++ programs.  Panda3D is open-source and free for any purpose,
 including commercial ventures, thanks to its
 including commercial ventures, thanks to its
-[liberal license](https://www.panda3d.org/license.php).  To learn more about
-Panda3D's capabilities, visit the [gallery](https://www.panda3d.org/gallery.php)
-and the [feature list](https://www.panda3d.org/features.php).  To learn how to
-use Panda3D, check the [documentation](https://www.panda3d.org/documentation.php)
+[liberal license](https://www.panda3d.org/license/). To learn more about
+Panda3D's capabilities, visit the [gallery](https://www.panda3d.org/gallery/)
+and the [feature list](https://www.panda3d.org/features/).  To learn how to
+use Panda3D, check the [documentation](https://www.panda3d.org/documentation/)
 resources. If you get stuck, ask for help from our active
 resources. If you get stuck, ask for help from our active
-[community](https://www.panda3d.org/community.php).
+[community](https://discourse.panda3d.org).
 
 
 Panda3D is licensed under the Modified BSD License.  See the LICENSE file for
 Panda3D is licensed under the Modified BSD License.  See the LICENSE file for
 more details.
 more details.
@@ -21,7 +21,16 @@ more details.
 Installing Panda3D
 Installing Panda3D
 ==================
 ==================
 
 
-By far, the easiest way to install the latest development build of Panda3D
+The latest Panda3D SDK can be downloaded from
+(this page)[https://www.panda3d.org/download/sdk-1-10-0/].
+If you are familiar with installing Python packages, you can use
+the following comand:
+
+```bash
+pip install panda3d
+```
+
+The easiest way to install the latest development build of Panda3D
 into an existing Python installation is using the following command:
 into an existing Python installation is using the following command:
 
 
 ```bash
 ```bash
@@ -31,9 +40,7 @@ pip install --pre --extra-index-url https://archive.panda3d.org/ panda3d
 If this command fails, please make sure your version of pip is up-to-date.
 If this command fails, please make sure your version of pip is up-to-date.
 
 
 If you prefer to install the full SDK with all tools, the latest development
 If you prefer to install the full SDK with all tools, the latest development
-builds can be obtained from this page:
-
-https://www.panda3d.org/download.php?sdk&version=devel
+builds can be obtained from (this page)[https://www.panda3d.org/download/].
 
 
 These are automatically kept up-to-date with the latest GitHub version of Panda.
 These are automatically kept up-to-date with the latest GitHub version of Panda.
 
 
@@ -96,7 +103,7 @@ python makepanda/makepanda.py --everything --installer --no-egl --no-gles --no-g
 You will probably see some warnings saying that it's unable to find several
 You will probably see some warnings saying that it's unable to find several
 dependency packages.  You should determine which ones you want to include in
 dependency packages.  You should determine which ones you want to include in
 your build and install the respective development packages.  You may visit
 your build and install the respective development packages.  You may visit
-[this manual page](https://www.panda3d.org/manual/index.php/Dependencies)
+[this manual page](https://www.panda3d.org/manual/?title=Third-party_dependencies_and_license_info)
 for an overview of the various dependencies.
 for an overview of the various dependencies.
 
 
 If you are on Ubuntu, this command should cover the most frequently
 If you are on Ubuntu, this command should cover the most frequently

+ 15 - 4
dtool/src/prc/streamReader.cxx

@@ -26,6 +26,9 @@ get_string() {
 
 
   // First, get the length of the string
   // First, get the length of the string
   size_t size = get_uint16();
   size_t size = get_uint16();
+  if (size == 0) {
+    return string();
+  }
 
 
   char *buffer = (char *)alloca(size);
   char *buffer = (char *)alloca(size);
   _in->read(buffer, size);
   _in->read(buffer, size);
@@ -42,6 +45,9 @@ get_string32() {
 
 
   // First, get the length of the string
   // First, get the length of the string
   size_t size = get_uint32();
   size_t size = get_uint32();
+  if (size == 0) {
+    return string();
+  }
 
 
   char *buffer = (char *)PANDA_MALLOC_ARRAY(size);
   char *buffer = (char *)PANDA_MALLOC_ARRAY(size);
   _in->read(buffer, size);
   _in->read(buffer, size);
@@ -60,7 +66,7 @@ get_z_string() {
 
 
   string result;
   string result;
   int ch = _in->get();
   int ch = _in->get();
-  while (!_in->eof() && !_in->fail() && ch != '\0') {
+  while (!_in->fail() && ch != EOF && ch != '\0') {
     result += (char)ch;
     result += (char)ch;
     ch = _in->get();
     ch = _in->get();
   }
   }
@@ -76,6 +82,10 @@ string StreamReader::
 get_fixed_string(size_t size) {
 get_fixed_string(size_t size) {
   nassertr(!_in->eof() && !_in->fail(), string());
   nassertr(!_in->eof() && !_in->fail(), string());
 
 
+  if (size == 0) {
+    return string();
+  }
+
   char *buffer = (char *)alloca(size);
   char *buffer = (char *)alloca(size);
   _in->read(buffer, size);
   _in->read(buffer, size);
   size_t read_bytes = _in->gcount();
   size_t read_bytes = _in->gcount();
@@ -90,8 +100,9 @@ get_fixed_string(size_t size) {
  */
  */
 void StreamReader::
 void StreamReader::
 skip_bytes(size_t size) {
 skip_bytes(size_t size) {
-  nassertv(!_in->eof() && !_in->fail());
+  nassertv(!_in->fail());
   nassertv((int)size >= 0);
   nassertv((int)size >= 0);
+  nassertv(size == 0 || !_in->eof());
 
 
   while (size > 0) {
   while (size > 0) {
     _in->get();
     _in->get();
@@ -145,9 +156,9 @@ string StreamReader::
 readline() {
 readline() {
   string line;
   string line;
   int ch = _in->get();
   int ch = _in->get();
-  while (!_in->eof() && !_in->fail()) {
+  while (ch != EOF && !_in->fail()) {
     line += (char)ch;
     line += (char)ch;
-    if (ch == '\n') {
+    if (ch == '\n' || _in->eof()) {
       // Here's the newline character.
       // Here's the newline character.
       return line;
       return line;
     }
     }

+ 2 - 2
dtool/src/prc/streamReader_ext.cxx

@@ -45,9 +45,9 @@ readline() {
 
 
   std::string line;
   std::string line;
   int ch = in->get();
   int ch = in->get();
-  while (!in->eof() && !in->fail()) {
+  while (ch != EOF && !in->fail()) {
     line += ch;
     line += ch;
-    if (ch == '\n') {
+    if (ch == '\n' || in->eof()) {
       // Here's the newline character.
       // Here's the newline character.
       break;
       break;
     }
     }

+ 0 - 5
makepanda/makewheel.py

@@ -526,11 +526,6 @@ def makewheel(version, output_dir, platform=None):
 
 
     # Update relevant METADATA entries
     # Update relevant METADATA entries
     METADATA['version'] = version
     METADATA['version'] = version
-    version_classifiers = [
-        "Programming Language :: Python :: {0}".format(*sys.version_info),
-        "Programming Language :: Python :: {0}.{1}".format(*sys.version_info),
-    ]
-    METADATA['classifiers'].extend(version_classifiers)
 
 
     # Build out the metadata
     # Build out the metadata
     details = METADATA["extensions"]["python.details"]
     details = METADATA["extensions"]["python.details"]

+ 2 - 2
panda/src/downloader/decompressor.cxx

@@ -183,7 +183,7 @@ decompress(const Filename &source_file) {
     return false;
     return false;
 
 
   int ch = _decompress->get();
   int ch = _decompress->get();
-  while (!_decompress->eof() && !_decompress->fail()) {
+  while (ch != EOF && !_decompress->fail()) {
     _dest->put(ch);
     _dest->put(ch);
     ch = _decompress->get();
     ch = _decompress->get();
   }
   }
@@ -207,7 +207,7 @@ decompress(Ramfile &source_and_dest_file) {
   IDecompressStream decompress(&source, false);
   IDecompressStream decompress(&source, false);
 
 
   int ch = decompress.get();
   int ch = decompress.get();
-  while (!decompress.eof() && !decompress.fail()) {
+  while (ch != EOF && !decompress.fail()) {
     dest.put(ch);
     dest.put(ch);
     ch = decompress.get();
     ch = decompress.get();
   }
   }

+ 2 - 2
panda/src/downloader/documentSpec.cxx

@@ -66,7 +66,7 @@ input(std::istream &in) {
     // Scan the tag, up to but not including the closing paren.
     // Scan the tag, up to but not including the closing paren.
     std::string tag;
     std::string tag;
     in >> ch;
     in >> ch;
-    while (!in.fail() && !in.eof() && ch != ')') {
+    while (!in.fail() && ch != EOF && ch != ')') {
       tag += ch;
       tag += ch;
       // We want to include embedded whitespace, so we use get().
       // We want to include embedded whitespace, so we use get().
       ch = in.get();
       ch = in.get();
@@ -81,7 +81,7 @@ input(std::istream &in) {
   // Scan the date, up to but not including the closing bracket.
   // Scan the date, up to but not including the closing bracket.
   if (ch != ']') {
   if (ch != ']') {
     std::string date;
     std::string date;
-    while (!in.fail() && !in.eof() && ch != ']') {
+    while (!in.fail() && ch != EOF && ch != ']') {
       date += ch;
       date += ch;
       ch = in.get();
       ch = in.get();
     }
     }

+ 2 - 2
panda/src/downloader/httpChannel.cxx

@@ -2762,7 +2762,7 @@ bool HTTPChannel::
 server_getline(string &str) {
 server_getline(string &str) {
   nassertr(!_source.is_null(), false);
   nassertr(!_source.is_null(), false);
   int ch = (*_source)->get();
   int ch = (*_source)->get();
-  while (!(*_source)->eof() && !(*_source)->fail()) {
+  while (ch != EOF && !(*_source)->fail()) {
     switch (ch) {
     switch (ch) {
     case '\n':
     case '\n':
       // end-of-line character, we're done.
       // end-of-line character, we're done.
@@ -2850,7 +2850,7 @@ bool HTTPChannel::
 server_get(string &str, size_t num_bytes) {
 server_get(string &str, size_t num_bytes) {
   nassertr(!_source.is_null(), false);
   nassertr(!_source.is_null(), false);
   int ch = (*_source)->get();
   int ch = (*_source)->get();
-  while (!(*_source)->eof() && !(*_source)->fail()) {
+  while (ch != EOF && !(*_source)->fail()) {
     _working_get += (char)ch;
     _working_get += (char)ch;
     if (_working_get.length() >= num_bytes) {
     if (_working_get.length() >= num_bytes) {
       str = _working_get;
       str = _working_get;

+ 1 - 1
panda/src/downloader/httpDate.cxx

@@ -279,7 +279,7 @@ input(std::istream &in) {
 
 
   string date;
   string date;
   ch = in.get();
   ch = in.get();
-  while (!in.fail() && !in.eof() && ch != '"') {
+  while (!in.fail() && ch != EOF && ch != '"') {
     date += ch;
     date += ch;
     ch = in.get();
     ch = in.get();
   }
   }

+ 1 - 1
panda/src/downloader/socketStream.cxx

@@ -56,7 +56,7 @@ do_receive_datagram(Datagram &dg) {
     // Read the first two bytes: the datagram length.
     // Read the first two bytes: the datagram length.
     while ((int)_data_so_far.size() < _tcp_header_size) {
     while ((int)_data_so_far.size() < _tcp_header_size) {
       int ch = _istream->get();
       int ch = _istream->get();
-      if (_istream->eof() || _istream->fail()) {
+      if (ch == EOF || _istream->fail()) {
         _istream->clear();
         _istream->clear();
         return false;
         return false;
       }
       }

+ 2 - 2
panda/src/express/hashVal.cxx

@@ -50,7 +50,7 @@ input_hex(istream &in) {
   size_t i = 0;
   size_t i = 0;
   int ch = in.get();
   int ch = in.get();
 
 
-  while (!in.eof() && !in.fail() && isxdigit(ch)) {
+  while (ch != EOF && !in.fail() && isxdigit(ch)) {
     if (i < 32) {
     if (i < 32) {
       buffer[i] = (char)ch;
       buffer[i] = (char)ch;
     }
     }
@@ -63,7 +63,7 @@ input_hex(istream &in) {
     return;
     return;
   }
   }
 
 
-  if (!in.eof()) {
+  if (ch != EOF) {
     in.putback((char)ch);
     in.putback((char)ch);
   } else {
   } else {
     in.clear();
     in.clear();

+ 1 - 1
panda/src/express/make_ca_bundle.cxx

@@ -108,7 +108,7 @@ main(int argc, char *argv[]) {
   int col = 0;
   int col = 0;
   unsigned int ch;
   unsigned int ch;
   ch = in.get();
   ch = in.get();
-  while (!in.fail() && !in.eof()) {
+  while (!in.fail() && ch != EOF) {
     if (col == 0) {
     if (col == 0) {
       out << "\n  ";
       out << "\n  ";
     } else if (col == col_width) {
     } else if (col == col_width) {

+ 4 - 5
panda/src/express/multifile.cxx

@@ -1785,8 +1785,7 @@ compare_subfile(int index, const Filename &filename) {
   in2.seekg(0);
   in2.seekg(0);
   int byte1 = in1->get();
   int byte1 = in1->get();
   int byte2 = in2.get();
   int byte2 = in2.get();
-  while (!in1->fail() && !in1->eof() &&
-         !in2.fail() && !in2.eof()) {
+  while (!in1->fail() && !in2.fail()) {
     if (byte1 != byte2) {
     if (byte1 != byte2) {
       close_read_subfile(in1);
       close_read_subfile(in1);
       return false;
       return false;
@@ -2497,7 +2496,7 @@ read_index(istream &read, streampos fpos, Multifile *multifile) {
   StreamReader reader(read);
   StreamReader reader(read);
 
 
   streampos next_index = multifile->word_to_streampos(reader.get_uint32());
   streampos next_index = multifile->word_to_streampos(reader.get_uint32());
-  if (read.eof() || read.fail()) {
+  if (read.fail()) {
     _flags |= SF_index_invalid;
     _flags |= SF_index_invalid;
     return 0;
     return 0;
   }
   }
@@ -2529,7 +2528,7 @@ read_index(istream &read, streampos fpos, Multifile *multifile) {
   }
   }
 
 
   size_t name_length = reader.get_uint16();
   size_t name_length = reader.get_uint16();
-  if (read.eof() || read.fail()) {
+  if (read.fail()) {
     _flags |= SF_index_invalid;
     _flags |= SF_index_invalid;
     return 0;
     return 0;
   }
   }
@@ -2543,7 +2542,7 @@ read_index(istream &read, streampos fpos, Multifile *multifile) {
   _name = string(name_buffer, name_length);
   _name = string(name_buffer, name_length);
   PANDA_FREE_ARRAY(name_buffer);
   PANDA_FREE_ARRAY(name_buffer);
 
 
-  if (read.eof() || read.fail()) {
+  if (read.fail()) {
     _flags |= SF_index_invalid;
     _flags |= SF_index_invalid;
     return 0;
     return 0;
   }
   }

+ 2 - 2
panda/src/gobj/texture.cxx

@@ -4158,7 +4158,7 @@ do_read_dds(CData *cdata, istream &in, const string &filename, bool header_only)
     cdata->_num_mipmap_levels_read = cdata->_ram_images.size();
     cdata->_num_mipmap_levels_read = cdata->_ram_images.size();
   }
   }
 
 
-  if (in.fail() || in.eof()) {
+  if (in.fail()) {
     gobj_cat.error()
     gobj_cat.error()
       << filename << ": truncated DDS file.\n";
       << filename << ": truncated DDS file.\n";
     return false;
     return false;
@@ -4924,7 +4924,7 @@ do_read_ktx(CData *cdata, istream &in, const string &filename, bool header_only)
     }
     }
   }
   }
 
 
-  if (in.fail() || in.eof()) {
+  if (in.fail()) {
     gobj_cat.error()
     gobj_cat.error()
       << filename << ": truncated KTX file.\n";
       << filename << ": truncated KTX file.\n";
     return false;
     return false;

+ 4 - 4
panda/src/pnmimage/pnmimage_base.cxx

@@ -120,7 +120,7 @@ int
 pm_readbigshort(istream *in, short *sP) {
 pm_readbigshort(istream *in, short *sP) {
   StreamReader reader(in, false);
   StreamReader reader(in, false);
   *sP = reader.get_be_int16();
   *sP = reader.get_be_int16();
-  return (!in->eof() && !in->fail()) ? 0 : -1;
+  return (!in->fail()) ? 0 : -1;
 }
 }
 
 
 int
 int
@@ -134,7 +134,7 @@ int
 pm_readbiglong(istream *in, long *lP) {
 pm_readbiglong(istream *in, long *lP) {
   StreamReader reader(in, false);
   StreamReader reader(in, false);
   *lP = reader.get_be_int32();
   *lP = reader.get_be_int32();
-  return (!in->eof() && !in->fail()) ? 0 : -1;
+  return (!in->fail()) ? 0 : -1;
 }
 }
 
 
 int
 int
@@ -148,7 +148,7 @@ int
 pm_readlittleshort(istream *in, short *sP) {
 pm_readlittleshort(istream *in, short *sP) {
   StreamReader reader(in, false);
   StreamReader reader(in, false);
   *sP = reader.get_int16();
   *sP = reader.get_int16();
-  return (!in->eof() && !in->fail()) ? 0 : -1;
+  return (!in->fail()) ? 0 : -1;
 }
 }
 
 
 int
 int
@@ -162,7 +162,7 @@ int
 pm_readlittlelong(istream *in, long *lP) {
 pm_readlittlelong(istream *in, long *lP) {
   StreamReader reader(in, false);
   StreamReader reader(in, false);
   *lP = reader.get_int32();
   *lP = reader.get_int32();
-  return (!in->eof() && !in->fail()) ? 0 : -1;
+  return (!in->fail()) ? 0 : -1;
 }
 }
 
 
 int
 int

+ 1 - 1
pandatool/src/miscprogs/binToC.cxx

@@ -101,7 +101,7 @@ run() {
   int col = 0;
   int col = 0;
   unsigned int ch;
   unsigned int ch;
   ch = in.get();
   ch = in.get();
-  while (!in.fail() && !in.eof()) {
+  while (!in.fail() && ch != EOF) {
     if (col == 0) {
     if (col == 0) {
       out << "\n  ";
       out << "\n  ";
     } else if (col == col_width) {
     } else if (col == col_width) {

+ 11 - 0
setup.cfg

@@ -13,10 +13,21 @@ classifiers =
     Operating System :: OS Independent
     Operating System :: OS Independent
     Programming Language :: C++
     Programming Language :: C++
     Programming Language :: Python
     Programming Language :: Python
+    Programming Language :: Python :: 2
+    Programming Language :: Python :: 2.7
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.4
+    Programming Language :: Python :: 3.5
+    Programming Language :: Python :: 3.6
+    Programming Language :: Python :: 3.7
+    Programming Language :: Python :: Implementation :: CPython
     Topic :: Games/Entertainment
     Topic :: Games/Entertainment
     Topic :: Multimedia
     Topic :: Multimedia
     Topic :: Multimedia :: Graphics
     Topic :: Multimedia :: Graphics
     Topic :: Multimedia :: Graphics :: 3D Rendering
     Topic :: Multimedia :: Graphics :: 3D Rendering
+    Topic :: Software Development :: Libraries
+    Topic :: Software Development :: Libraries :: Application Frameworks
+    Topic :: Software Development :: Libraries :: Python Modules
 author = Panda3D Team
 author = Panda3D Team
 author_email = [email protected]
 author_email = [email protected]
 
 

+ 12 - 0
tests/express/test_multifile.py

@@ -0,0 +1,12 @@
+from panda3d.core import Multifile, StringStream, IStreamWrapper
+
+
+def test_multifile_read_empty():
+    stream = StringStream(b'pmf\x00\n\r\x01\x00\x01\x00\x01\x00\x00\x00\xdb\x9d7\\\x00\x00\x00\x00')
+    wrapper = IStreamWrapper(stream)
+
+    m = Multifile()
+    assert m.open_read(wrapper)
+    assert m.is_read_valid()
+    assert m.get_num_subfiles() == 0
+    m.close()

+ 157 - 0
tests/prc/test_stream_reader.py

@@ -0,0 +1,157 @@
+from panda3d.core import StreamReader, StringStream
+import pytest
+
+
+def test_streamreader_string():
+    # Empty string
+    stream = StringStream(b'\x00\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_string() == ''
+
+    # String size but no string contents
+    stream = StringStream(b'\x01\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_string() == ''
+
+    # String of length 1
+    stream = StringStream(b'\x01\x00A')
+    reader = StreamReader(stream, False)
+    assert reader.get_string() == 'A'
+
+    # String with excess data
+    stream = StringStream(b'\x01\x00AB')
+    reader = StreamReader(stream, False)
+    assert reader.get_string() == 'A'
+
+    # EOF before end of string
+    stream = StringStream(b'\x03\x00AB')
+    reader = StreamReader(stream, False)
+    assert reader.get_string() == 'AB'
+
+    # Preserves null bytes
+    stream = StringStream(b'\x02\x00\x00\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_string() == '\x00\x00'
+
+
+def test_streamreader_string32():
+    # Empty string
+    stream = StringStream(b'\x00\x00\x00\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_string32() == ''
+
+    # String size but no string contents
+    stream = StringStream(b'\x01\x00\x00\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_string32() == ''
+
+    # String of length 1
+    stream = StringStream(b'\x01\x00\x00\x00A')
+    reader = StreamReader(stream, False)
+    assert reader.get_string32() == 'A'
+
+    # String with excess data
+    stream = StringStream(b'\x01\x00\x00\x00AB')
+    reader = StreamReader(stream, False)
+    assert reader.get_string32() == 'A'
+
+    # EOF before end of string
+    stream = StringStream(b'\x04\x00\x00\x00AB')
+    reader = StreamReader(stream, False)
+    assert reader.get_string32() == 'AB'
+
+    # Preserves null bytes
+    stream = StringStream(b'\x02\x00\x00\x00\x00\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_string32() == '\x00\x00'
+
+
+def test_streamreader_z_string():
+    # Empty stream
+    stream = StringStream(b'')
+    reader = StreamReader(stream, False)
+    assert reader.get_z_string() == ''
+
+    # Empty string
+    stream = StringStream(b'\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_z_string() == ''
+
+    # String of length 1
+    stream = StringStream(b'A\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_z_string() == 'A'
+
+    # String with excess data
+    stream = StringStream(b'ABC\x00AB')
+    reader = StreamReader(stream, False)
+    assert reader.get_z_string() == 'ABC'
+
+    # EOF before end of string
+    stream = StringStream(b'ABC')
+    reader = StreamReader(stream, False)
+    assert reader.get_z_string() == 'ABC'
+
+
+def test_streamreader_fixed_string():
+    # Zero-length string
+    stream = StringStream(b'ABC')
+    reader = StreamReader(stream, False)
+    assert reader.get_fixed_string(0) == ''
+
+    # Empty stream
+    stream = StringStream(b'')
+    reader = StreamReader(stream, False)
+    assert reader.get_fixed_string(1) == ''
+
+    # Empty string
+    stream = StringStream(b'\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_fixed_string(1) == ''
+
+    # String of length 1
+    stream = StringStream(b'A')
+    reader = StreamReader(stream, False)
+    assert reader.get_fixed_string(1) == 'A'
+
+    # String of length 1, excess data
+    stream = StringStream(b'ABC\x00')
+    reader = StreamReader(stream, False)
+    assert reader.get_fixed_string(1) == 'A'
+
+    # EOF before end of string
+    stream = StringStream(b'AB')
+    reader = StreamReader(stream, False)
+    assert reader.get_fixed_string(4) == 'AB'
+
+
+def test_streamreader_readline():
+    # Empty stream
+    stream = StringStream(b'')
+    reader = StreamReader(stream, False)
+    assert reader.readline() == b''
+    assert reader.readline() == b''
+
+    # Single line without newline
+    stream = StringStream(b'A')
+    reader = StreamReader(stream, False)
+    assert reader.readline() == b'A'
+    assert reader.readline() == b''
+
+    # Single newline
+    stream = StringStream(b'\n')
+    reader = StreamReader(stream, False)
+    assert reader.readline() == b'\n'
+    assert reader.readline() == b''
+
+    # Line with text followed by empty line
+    stream = StringStream(b'A\n\n')
+    reader = StreamReader(stream, False)
+    assert reader.readline() == b'A\n'
+    assert reader.readline() == b'\n'
+    assert reader.readline() == b''
+
+    # Preserve null byte
+    stream = StringStream(b'\x00\x00')
+    reader = StreamReader(stream, False)
+    assert reader.readline() == b'\x00\x00'