1
0
mirror of https://github.com/chylex/Brotli-Builder.git synced 2025-04-13 00:15:42 +02:00

Add BitStream w/ unit tests

This commit is contained in:
chylex 2018-11-15 11:49:31 +01:00
parent 74a926f279
commit c5fa6fcc37
3 changed files with 350 additions and 0 deletions

214
BrotliLib/IO/BitStream.cs Normal file
View File

@ -0,0 +1,214 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BrotliLib.IO{
public class BitStream : IEnumerable<bool>{
private const char False = '0';
private const char True = '1';
private const int ByteSize = 8;
private const int BytesPerEntry = sizeof(ulong);
private const int BitEntrySize = ByteSize * BytesPerEntry;
// Instance
public int Length { get; private set; }
private readonly LinkedList<ulong> bitCollection = new LinkedList<ulong>();
private LinkedListNode<ulong> lastNode;
private int lastNodeIndex;
#region Construction
/// <summary>
/// Initializes an empty <see cref="BitStream"/>.
/// </summary>
public BitStream(){
this.lastNode = this.bitCollection.AddLast(0L);
this.lastNodeIndex = 0;
}
/// <summary>
/// Initializes a <see cref="BitStream"/> from a string consisting of 0s and 1s.
/// </summary>
/// <param name="bits">Input string. Must be either empty, or only contain the characters 0 and 1.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the input <paramref name="bits"/> string contains a character that is not 0 or 1.</exception>
public BitStream(string bits) : this(){
foreach(char chr in bits){
switch(chr){
case False: this.Add(false); break;
case True: this.Add(true); break;
default: throw new ArgumentOutOfRangeException(nameof(bits), "Invalid character found in input string: "+chr);
}
}
}
/// <summary>
/// Initializes a <see cref="BitStream"/> from a byte array.
/// </summary>
/// <param name="bytes">Input byte array segment.</param>
public BitStream(byte[] bytes) : this(){
int index = 0;
foreach(byte value in bytes){
int offset = index % BytesPerEntry;
if (offset == 0 && index > 0){
this.lastNode = this.bitCollection.AddLast(0L);
this.lastNodeIndex += BitEntrySize;
}
this.lastNode.Value |= (ulong)value << (ByteSize * offset);
++index;
}
this.Length = ByteSize * index;
}
/// <summary>
/// Initializes a new <see cref="BitStream"/> as a clone of <paramref name="source"/>.
/// </summary>
/// <param name="source">Source stream.</param>
private BitStream(BitStream source){
foreach(ulong bitEntry in source.bitCollection){
this.bitCollection.AddLast(bitEntry);
}
this.lastNode = this.bitCollection.Last;
this.lastNodeIndex = source.lastNodeIndex;
this.Length = source.Length;
}
/// <summary>
/// Returns a copy of the object, which can be modified without affecting the original object.
/// </summary>
public BitStream Clone(){
return new BitStream(this);
}
#endregion
#region Mutation
/// <summary>
/// Appends a bit to the end of the stream.
/// </summary>
/// <param name="bit">Input bit.</param>
public void Add(bool bit){
int offset = Length - lastNodeIndex;
if (offset >= BitEntrySize){
lastNode = bitCollection.AddLast(0L);
lastNodeIndex += BitEntrySize;
offset -= BitEntrySize;
}
if (bit){
lastNode.Value |= 1UL << offset;
}
else{
lastNode.Value &= lastNode.Value & ~(1UL << offset);
}
++Length;
}
#endregion
#region Enumeration
/// <summary>
/// Returns an enumerator that traverses the stream, converting 0s to false, and 1s to true.
/// </summary>
public IEnumerator<bool> GetEnumerator(){
int bitsLeft = Length;
foreach(ulong bitEntry in bitCollection){
for(int bitIndex = 0; bitIndex < BitEntrySize; bitIndex++){
if (--bitsLeft < 0){
yield break;
}
yield return (bitEntry & (1UL << bitIndex)) != 0;
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
#region Conversion
/// <summary>
/// Converts the stream into a byte array, with zero padding at the end if needed.
/// </summary>
public byte[] ToByteArray(){
const int ByteMask = (1 << ByteSize) - 1;
byte[] bytes = new byte[(Length + ByteSize - 1) / ByteSize];
int index = -1;
foreach(ulong bitEntry in bitCollection){
if (++index >= bytes.Length){
break;
}
bytes[index] = (byte)(bitEntry & ByteMask);
for(int byteOffset = 1; byteOffset < BytesPerEntry; byteOffset++){
if (++index >= bytes.Length){
break;
}
bytes[index] = (byte)((bitEntry >> (ByteSize * byteOffset)) & ByteMask);
}
}
return bytes;
}
/// <summary>
/// Converts the stream into its text representation. The returned string is empty if the stream is also empty, or contains a sequence 0s and 1s.
/// </summary>
public override string ToString(){
StringBuilder build = new StringBuilder(Length);
foreach(bool bit in this){
build.Append(bit ? True : False);
}
return build.ToString();
}
#endregion
#region Equality
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
public override int GetHashCode(){
int hash = Length * 17;
foreach(ulong bitEntry in bitCollection){
hash = unchecked((hash * 31) + (bitEntry.GetHashCode()));
}
return hash;
}
/// <summary>
/// Returns true if and only if the <paramref name="obj"/> parameter is a <see cref="BitStream"/> with the same <see cref="Length"/> and contents.
/// </summary>
/// <param name="obj"></param>
public override bool Equals(object obj){
return obj is BitStream other && Length == other.Length && bitCollection.SequenceEqual(other.bitCollection);
}
#endregion
}
}

View File

@ -0,0 +1,135 @@
namespace UnitTests.IO.TestBitStream
open Xunit
open System
open BrotliLib.IO
module Representations =
[<Theory>]
[<InlineData((* 0*)"")>]
[<InlineData((* 1*)"0")>]
[<InlineData((* 2*)"00")>]
[<InlineData((* 3*)"000")>]
[<InlineData((* 4*)"0000")>]
[<InlineData((* 8*)"11110000")>]
[<InlineData((* 16*)"1111000011110000")>]
[<InlineData((* 32*)"11110000111100001111000011110000")>]
[<InlineData((* 64*)"1111000011110000111100001111000011110000111100001111000011110000")>]
[<InlineData((*128*)"11110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000")>]
[<InlineData((*129*)"111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001")>]
let ``constructing from string yields same length and string representation`` (bits: string) =
let stream = BitStream(bits)
Assert.Equal(bits.Length, stream.Length)
Assert.Equal(bits, stream.ToString())
[<Theory>]
[<InlineData("1100 ")>]
[<InlineData("1111211")>]
[<InlineData("0000_1111")>]
let ``constructing from string with invalid characters throws exception`` (bits: string) =
Assert.Throws<ArgumentOutOfRangeException>(fun () -> BitStream(bits) |> ignore)
[<Theory>]
[<InlineData()>]
[<InlineData(0b00000000uy)>]
[<InlineData(0b11100110uy)>]
[<InlineData(0b00000000uy, 0b11111111uy)>]
[<InlineData(0b11111111uy, 0b00000000uy, 0b11111111uy)>]
[<InlineData(0b00000000uy, 0b11111111uy, 0b00000000uy, 0b11111111uy)>]
[<InlineData(0b11111111uy, 0b00000000uy, 0b11111111uy, 0b00000000uy, 0b11111111uy)>]
[<InlineData(0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy, 0b01010101uy, 0b10101010uy)>]
let ``constructing from byte array yields same length and byte array representation`` ([<ParamArray>] bytes: byte array) =
let stream = BitStream(bytes)
Assert.Equal(bytes.Length * 8, stream.Length)
Assert.Equal<byte array>(bytes, stream.ToByteArray())
[<Theory>]
[<InlineData("0", 0b00000000uy)>]
[<InlineData("1", 0b00000001uy)>]
[<InlineData("11", 0b00000011uy)>]
[<InlineData("1111111", 0b01111111uy)>]
[<InlineData("11111111", 0b11111111uy)>]
[<InlineData("01010101", 0b10101010uy)>]
[<InlineData("111111110", 0b11111111uy, 0b00000000uy)>]
[<InlineData("111111111", 0b11111111uy, 0b00000001uy)>]
[<InlineData("11110000111100001111001", 0b00001111uy, 0b00001111uy, 0b01001111uy)>]
let ``constructing from string yields correct byte array representation with zero padding`` (bits: string, [<ParamArray>] bytes: byte array) =
Assert.Equal<byte array>(bytes, BitStream(bits).ToByteArray())
module Equality =
let equal : obj array seq = seq {
yield [| "" |]
yield [| "0" |]
yield [| "1" |]
yield [| "10101010" |]
yield [| "000000000" |]
}
let notequal : obj array seq = seq {
yield [| ""; "0" |]
yield [| "0"; "1" |]
yield [| "00"; "000" |]
yield [| "10101010"; "01010101" |]
yield [| "00000000"; "0000000000" |]
yield [| "000000001"; "000000000" |]
}
[<Theory>]
[<MemberData("equal")>]
let ``two streams are equal if they have same contents`` (bits: string) =
Assert.True(BitStream(bits).Equals(BitStream(bits)))
[<Theory>]
[<MemberData("notequal")>]
let ``two streams are not equal if they have different contents`` (first: string, second: string) =
Assert.False(BitStream(first).Equals(BitStream(second)))
[<Theory>]
[<MemberData("equal")>]
let ``two equal streams have the same hash code`` (bits: string) =
Assert.Equal(BitStream(bits).GetHashCode(), BitStream(bits).GetHashCode())
[<Theory>]
[<MemberData("notequal")>]
let ``two different streams should generally have different hash codes`` (first: string, second: string) =
Assert.NotEqual(BitStream(first).GetHashCode(), BitStream(second).GetHashCode())
module Mutability =
[<Theory>]
[<InlineData("")>]
[<InlineData("1", true)>]
[<InlineData("0", false)>]
[<InlineData("10", true, false)>]
[<InlineData("11110000", true, true, true, true, false, false, false, false)>]
[<InlineData("11110000111100001", true, true, true, true, false, false, false, false, true, true, true, true, false, false, false, false, true)>]
let ``appending to empty stream yields correct text representation`` (expected: string, [<ParamArray>] values: bool array) =
let stream = BitStream()
Array.iter (stream.Add) <| values
Assert.Equal(expected, stream.ToString())
module Cloning =
[<Theory>]
[<InlineData("")>]
[<InlineData("0")>]
[<InlineData("1")>]
[<InlineData("101010101")>]
let ``cloned stream is equal to original stream but not the same object`` (bits: string) =
let original = BitStream(bits)
let clone = original.Clone()
Assert.Equal<BitStream>(original, clone)
Assert.NotSame(original, clone)
[<Fact>]
let ``appending to cloned stream keeps the original stream intact`` () =
let original = BitStream("11100")
let clone = original.Clone()
clone.Add(true)
Assert.Equal("11100", original.ToString())
Assert.Equal("111001", clone.ToString())

View File

@ -56,6 +56,7 @@
</Target>
<Import Project="..\packages\xunit.core.2.4.1\build\xunit.core.targets" Condition="Exists('..\packages\xunit.core.2.4.1\build\xunit.core.targets')" />
<ItemGroup>
<Compile Include="IO\TestBitStream.fs" />
<Content Include="packages.config" />
</ItemGroup>
<ItemGroup>