Skip to content

Commit

Permalink
Add utility for GSheets logger to stream output to Sheets (faucetsdn#…
Browse files Browse the repository at this point in the history
khyatimahendru authored Jan 28, 2025

Verified

This commit was signed with the committer’s verified signature. The key has expired.
Czaki Grzegorz Bokota
1 parent 8792d14 commit badb23d
Showing 12 changed files with 651 additions and 7 deletions.
6 changes: 3 additions & 3 deletions .gencode_hash.txt
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ eeafb6e89a6c43816cfcb26244a95615a4ee75966e1c015cae5a13b6cfcb0078 gencode/docs/r
741b880216be3743f6747800a042f2dbd89f3b0344c6b0a965f4bc010f03a930 gencode/docs/schema_doc.css
878ea88206c974f40643c3cc430875f9c4e8c5e3fd6bcd6358bd3eb6d48699a9 gencode/docs/schema_doc.min.js
7ed934930aee763e0beebc349725ba3909115e8d346bb762f28bcbe745bb163a gencode/docs/schema_extras.js
0f16008af7116a182322736046b05c2718ac8b1f2cd755a60d28c9cfaeb6a8a9 gencode/docs/site_metadata.html
0da2fca987af8f37f82e93b8b3fd8b954e4fe185bfe1e1f55adee5fe7f8024a9 gencode/docs/site_metadata.html
1bbcbf2d1ec1bf4554846b1daf09fa09c08dd967fa1f91ebec93027662529a31 gencode/docs/state.html
b2398e7f9e352c47867e38f97168f3eb88e23280c7dd576f13e038c8c3f714c9 gencode/docs/state_mapping.html
b8ff4cdb4b79449166b1c6e09ac0e58273ae62865b47bf943fdd365739c62a4c gencode/docs/state_validation.html
@@ -121,7 +121,7 @@ fadf083200e73d46cfdec2f710d40e3d2bc4dbf05196e4d0900186e72b62d95e gencode/java/u
ad70168de2e1801e26f2c1a8c33a6169e236682de59fd3f6038a77ac264aca14 gencode/java/udmi/schema/SequenceValidationState.java
72e3a07ce890c73fccd3d0cf757d073746b60b722c94edd5425dc066951a048f gencode/java/udmi/schema/SetupUdmiConfig.java
e5ce202bcf889287faa7d2d262d2f53f2241a10c659a83687de478803bdab127 gencode/java/udmi/schema/SetupUdmiState.java
8fcb31df1070010da6d3ae61d741a77d2ce8b3757226bbe35ff9218a0581b1ec gencode/java/udmi/schema/SiteLinks.java
2f802c738dd4c19211af8b6dec71b7659c51177c2774cfd34fc07e617bbe5b3e gencode/java/udmi/schema/SiteLinks.java
a3dc7f14d4270ea370db2015785ad12d4bd61825213ab7ac4ee03dda21dde4f2 gencode/java/udmi/schema/SiteLocation.java
4e51c21fcdb25e729258c428f55be4ac4909f526fdc3384734b6f5a87f1ef5cf gencode/java/udmi/schema/SiteMetadata.java
dc2fd582fffefbdeaf3bf7724ca34e94475c3cc820891ea4cc829847eb9ba79d gencode/java/udmi/schema/State.java
@@ -213,7 +213,7 @@ d59671d0bc2ac184e820b70c277cdf91af0a936a60cdffd9b3af0ea314f08adc gencode/python
6c5f3dd1c5ca9d821e3c48298af118fc7eafd97af9265dfd34b2ed8642efca77 gencode/python/udmi/schema/persistent_device.py
a58f8c98e837a5b56126ca0f410e02f1e9cfcd80a8cb429e0ef522defab1f690 gencode/python/udmi/schema/properties.py
a84e00db471b2038a473c2c83d72ea59b02c3d2bab56d38bc22435309f01e763 gencode/python/udmi/schema/query_cloud.py
d42bae60bd667cda893d3670896ed087f20df629635949cf418ee9be2e975347 gencode/python/udmi/schema/site_metadata.py
77bcd0622552871b9b1543f26c4fe961955813f64a8702f4efe894fba91ce116 gencode/python/udmi/schema/site_metadata.py
e604cf0280fe772de5f4e5ecf10dc6c564b6177eeff9cd9fb8b385af8fe10a95 gencode/python/udmi/schema/state.py
4a908cee3fb8afb559bcbfa594e57dbc515a35e4468e02600751b2fcce05a238 gencode/python/udmi/schema/state_blobset.py
182e07b534403dcc121d980672e41b0fa2ee55c4da1f5c56f0dad5d599450c80 gencode/python/udmi/schema/state_blobset_blob.py
20 changes: 19 additions & 1 deletion bin/toolrun
Original file line number Diff line number Diff line change
@@ -28,4 +28,22 @@ fi

cmd="java -cp $UDMI_JAR $JDWP $JAVA_CLASS $util_name $@"
echo $cmd
$cmd 2>&1 | tee $OUT_DIR/$util_name.log

site_path=$(realpath "$1")
if [[ ! -d $site_path ]]; then
site_path=$(dirname "$site_path")
fi

if [[ -e "$site_path/site_metadata.json" ]]; then
spreadsheet=$(jq -r '.sheet' "$site_path/site_metadata.json")
else
spreadsheet=
fi

if [[ $spreadsheet != "null" && -n "$spreadsheet" ]]; then
spreadsheet_id=$(echo "$spreadsheet" | grep -oP '(?<=/d/)[^/]+')
echo "Streaming logs to gsheet id $spreadsheet_id"
$cmd 2>&1 | tee $OUT_DIR/$util_name.log | stream_to_gsheets "$util_name" "$spreadsheet_id"
else
$cmd 2>&1 | tee $OUT_DIR/$util_name.log
fi
8 changes: 8 additions & 0 deletions etc/shell_common.sh
Original file line number Diff line number Diff line change
@@ -104,3 +104,11 @@ export UDMI_VERSION=$udmi_version
export UDMI_COMMIT=$udmi_commit
export UDMI_TIMEVER=$udmi_timever
export UDMI_REV=$udmi_rev

function stream_to_gsheets {
local tool_name=$1
local sheet_id=$2
timestamp=$(date +%Y%m%d_%H%M%S)
java -cp "$UDMI_JAR" "com.google.udmi.util.SheetsOutputStream" "$tool_name" \
"$sheet_id" "$tool_name.$timestamp.log"
}
45 changes: 45 additions & 0 deletions gencode/docs/site_metadata.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions gencode/java/udmi/schema/SiteLinks.java

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions gencode/python/udmi/schema/site_metadata.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions schema/site_metadata.json
Original file line number Diff line number Diff line change
@@ -92,6 +92,11 @@
"description": "Source repository where the UDMI site model is stored",
"type": "string",
"examples": ["https://github.com/faucetsdn/udmi_site_model", "[email protected]:faucetsdn/udmi_site_model.git"]
},
"sheet": {
"description": "Link to a spreadsheet to observe real-time output from any tool",
"type": "string",
"examples": ["https://docs.google.com/spreadsheets/d/<spreadsheet_id>"]
}
}
},
3 changes: 3 additions & 0 deletions udmis/build.gradle
Original file line number Diff line number Diff line change
@@ -111,4 +111,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
testImplementation 'org.mockito:mockito-core:5.3.1'

implementation 'com.google.oauth-client:google-oauth-client-jetty:1.20.0'
implementation 'com.google.apis:google-api-services-sheets:v4-rev484-1.20.0'
}
8 changes: 8 additions & 0 deletions validator/build.gradle
Original file line number Diff line number Diff line change
@@ -104,4 +104,12 @@ dependencies {
implementation 'com.google.cloud:google-cloud-firestore:0.84.0-beta'
implementation group: 'junit', name: 'junit', version: '4.13.2'
implementation 'org.jetbrains:annotations:20.1.0'

implementation 'com.google.oauth-client:google-oauth-client-jetty:1.20.0'
implementation 'com.google.apis:google-api-services-sheets:v4-rev484-1.20.0'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.mockito:mockito-junit-jupiter:5.6.0'
}
50 changes: 50 additions & 0 deletions validator/src/main/java/com/google/udmi/util/DualOutputStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.google.udmi.util;

import java.io.IOException;
import java.io.OutputStream;
import org.jetbrains.annotations.NotNull;

/**
* An OutputStream that duplicates output to two output streams, useful for scenarios such as
* logging to two destinations simultaneously.
*/
public class DualOutputStream extends OutputStream {

private final OutputStream primary;
private final OutputStream secondary;

public DualOutputStream(OutputStream primary, OutputStream secondary) {
this.primary = primary;
this.secondary = secondary;
}

@Override
public void write(int i) throws IOException {
primary.write(i);
secondary.write(i);
}

@Override
public void write(byte @NotNull [] b) throws IOException {
primary.write(b);
secondary.write(b);
}

@Override
public void write(byte @NotNull [] b, int off, int len) throws IOException {
primary.write(b, off, len);
secondary.write(b, off, len);
}

@Override
public void flush() throws IOException {
primary.flush();
secondary.flush();
}

@Override
public void close() throws IOException {
primary.close();
secondary.close();
}
}
256 changes: 256 additions & 0 deletions validator/src/main/java/com/google/udmi/util/SheetsOutputStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package com.google.udmi.util;

import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.SheetsScopes;
import com.google.api.services.sheets.v4.model.AddSheetRequest;
import com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetRequest;
import com.google.api.services.sheets.v4.model.Request;
import com.google.api.services.sheets.v4.model.SheetProperties;
import com.google.api.services.sheets.v4.model.Spreadsheet;
import com.google.api.services.sheets.v4.model.ValueRange;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.security.GeneralSecurityException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* Generic utility to log messages to Google Sheets. This class extends {@link OutputStream} and
* redirects output to a specified sheet in a Google Spreadsheet.
*/
public class SheetsOutputStream extends OutputStream {

private static final Logger LOGGER = LoggerFactory.getLogger(SheetsOutputStream.class);
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
static final List<String> SCOPES = Collections.singletonList(SheetsScopes.SPREADSHEETS);
private static final long DEFAULT_SYNC_TIME = 2000;
private static final NetHttpTransport HTTP_TRANSPORT;

static {
try {
HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
} catch (GeneralSecurityException | IOException e) {
throw new ExceptionInInitializerError("Error initializing HTTP Transport: " + e.getMessage());
}
}

final long syncTime;
private long lastWriteMillis = 0;
private final String applicationName;
private final String spreadsheetId;
private final String outputSheetTitle;
private final Sheets sheetsService;
final StringBuilder buffer = new StringBuilder();
private PrintStream originalSystemOut;
private PrintStream originalSystemErr;

/**
* Constructs a new `GSheetsOutputStream` with default sync time.
*
* @param applicationName The name of the application using the Google Sheets API.
* @param spreadsheetId The ID of the Google Spreadsheet.
* @param outputSheetTitle The title of the sheet where the output will be written.
* @throws IOException If there's an error creating the Google Sheets service or adding the sheet.
*/
public SheetsOutputStream(String applicationName, String spreadsheetId, String outputSheetTitle)
throws IOException {
this(applicationName, spreadsheetId, outputSheetTitle, DEFAULT_SYNC_TIME);
}

/**
* Constructs a new `GSheetsOutputStream`.
*
* @param applicationName The name of the application using the Google Sheets API.
* @param spreadsheetId The ID of the Google Spreadsheet.
* @param outputSheetTitle The title of the sheet where the output will be written.
* @param syncTime The time in milliseconds to wait before syncing the buffer to the sheet.
* @throws IOException If there's an error creating the Google Sheets service or adding the sheet.
*/
public SheetsOutputStream(String applicationName, String spreadsheetId, String outputSheetTitle,
long syncTime)
throws IOException {
this.applicationName = applicationName;
this.spreadsheetId = spreadsheetId;
this.outputSheetTitle = outputSheetTitle;
this.sheetsService = createSheetsService();
this.syncTime = syncTime;
addOutputSheet();
}

@Override
public void write(int i) {
buffer.append((char) i);
syncIfNeeded();
}

@Override
public void write(byte @NotNull [] b, int off, int len) {
buffer.append(new String(b, off, len));
syncIfNeeded();
}

private void syncIfNeeded() {
long currentTimeMillis = Instant.now().toEpochMilli();
if (buffer.indexOf("\n") != -1 && currentTimeMillis - lastWriteMillis >= syncTime) {
lastWriteMillis = currentTimeMillis;
appendToSheet();
}
}

void appendToSheet() {
String content = buffer.toString();
if (content.trim().isEmpty()) {
buffer.setLength(0); // Clear buffer even if nothing to write
return;
}
try {
List<List<Object>> values = Arrays.stream(content.split("\\n"))
.filter(line -> !line.trim().isEmpty()) // Filter out empty lines
.map(line -> Collections.singletonList((Object) line))
.collect(Collectors.toList());

if (!values.isEmpty()) {
ValueRange appendBody = new ValueRange().setValues(values);
sheetsService.spreadsheets().values()
.append(spreadsheetId, outputSheetTitle, appendBody)
.setValueInputOption("RAW").execute();
}
buffer.setLength(0);
} catch (IOException e) {
LOGGER.error("Error appending to sheet", e);
}
}

Sheets createSheetsService() throws IOException {
GoogleCredentials credential =
GoogleCredentials.getApplicationDefault().createScoped(SCOPES);
return new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, new HttpCredentialsAdapter(credential))
.setApplicationName(this.applicationName)
.build();
}


private boolean outputSheetExists() throws IOException {
Spreadsheet spreadsheet = sheetsService.spreadsheets().get(spreadsheetId).execute();
return spreadsheet.getSheets().stream()
.anyMatch(sheet -> sheet.getProperties().getTitle().equalsIgnoreCase(outputSheetTitle));
}

private void addOutputSheet() throws IOException {
if (!outputSheetExists()) {
BatchUpdateSpreadsheetRequest body = new BatchUpdateSpreadsheetRequest()
.setRequests(List.of(new Request().setAddSheet(new AddSheetRequest()
.setProperties(new SheetProperties().setTitle(outputSheetTitle)))));
try {
sheetsService.spreadsheets().batchUpdate(spreadsheetId, body).execute();
} catch (IOException e) {
throw new IOException("Failed to add output sheet: " + outputSheetTitle, e);
}
}
}

/**
* Redirects `System.out` and `System.err` to google sheets.
*/
public void startStream() {
if (originalSystemOut == null) {
originalSystemOut = System.out;
originalSystemErr = System.err;
DualOutputStream tee = new DualOutputStream(originalSystemOut, this);
PrintStream printStream = new PrintStream(tee);
System.setOut(printStream);
System.setErr(printStream);
}
}

/**
* Restores `System.out` and `System.err` to their original streams.
*/
public void stopStream() {
if (originalSystemOut != null) {
System.setOut(originalSystemOut);
System.setErr(originalSystemErr);
originalSystemOut = null;
originalSystemErr = null;

// flush out
appendToSheet();
}
}

/**
* Redirects standard input to Google Sheet output.
*/
public void startStreamFromInput() {
try (
DualOutputStream tee = new DualOutputStream(System.out, this);
PrintStream printStream = new PrintStream(tee);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
) {
String line;
while ((line = reader.readLine()) != null) {
printStream.println(line);
}
} catch (IOException e) {
throw new RuntimeException("Exception while reading input", e);
} finally {
// added to ensure last batch is always appended & to prevent loss in case of exceptions
appendToSheet();
}
}

/**
* Main method for the class. Can be used to start the logger from command line.
*
* @param args Command line arguments. Required: applicationName, spreadsheetId, sheetTitle.
* Optional: syncTime.
*/
public static void main(String[] args) {
if (args.length < 3 || args.length > 4) {
System.err.println(
"Usage: GSheetsOutputStream <applicationName> <spreadsheetId> <sheetTitle> [<syncTime>]");
System.exit(1);
}

String applicationName = args[0];
String spreadsheetId = args[1];
String sheetTitle = args[2];
long syncTime = DEFAULT_SYNC_TIME;

if (args.length == 4) {
try {
syncTime = Long.parseLong(args[3]);
if (syncTime <= 0) {
System.err.println("Sync time should be greater than zero");
System.exit(1);
}
} catch (NumberFormatException e) {
System.err.println("Invalid sync time format: " + args[3]);
System.exit(1);
}
}

try (SheetsOutputStream sheetsOutputStream =
new SheetsOutputStream(applicationName, spreadsheetId, sheetTitle, syncTime)) {
sheetsOutputStream.startStreamFromInput();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package com.google.udmi.util;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.Sheets.Spreadsheets;
import com.google.api.services.sheets.v4.Sheets.Spreadsheets.BatchUpdate;
import com.google.api.services.sheets.v4.Sheets.Spreadsheets.Get;
import com.google.api.services.sheets.v4.Sheets.Spreadsheets.Values;
import com.google.api.services.sheets.v4.Sheets.Spreadsheets.Values.Append;
import com.google.api.services.sheets.v4.model.AppendValuesResponse;
import com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetRequest;
import com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetResponse;
import com.google.api.services.sheets.v4.model.Sheet;
import com.google.api.services.sheets.v4.model.SheetProperties;
import com.google.api.services.sheets.v4.model.Spreadsheet;
import com.google.api.services.sheets.v4.model.ValueRange;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.junit.jupiter.MockitoExtension;

/**
* Unit tests for SheetsOutputStream.java
*/
@ExtendWith(MockitoExtension.class)
public class SheetsOutputStreamTest {

private final Append mockAppend = mock(Append.class);
private final AppendValuesResponse mockAppendValuesResponse = mock(AppendValuesResponse.class);
private final BatchUpdate mockBatchUpdate = mock(BatchUpdate.class);
private final BatchUpdateSpreadsheetResponse mockBatchUpdateResponse = mock(
BatchUpdateSpreadsheetResponse.class);
private final Get mockSpreadsheetsGet = mock(Get.class);
private final Sheet mockSheet = new Sheet();
private final Sheets mockSheetsService = mock(Sheets.class);
private final Spreadsheet mockSpreadSheet = mock(Spreadsheet.class);
private final Spreadsheets mockSpreadsheets = mock(Spreadsheets.class);
private final Values mockValues = mock(Values.class);

private final String applicationName = "TestApp";
private final String spreadsheetId = "testSpreadsheetId";
private final String outputSheetTitle = "TestSheet";
private final long syncTime = 2000;
private SheetsOutputStream sheetsOutputStream;


/**
* Mock interactions with the gcloud API.
*/
@Before
public void setup() throws IOException {
mockSheet.setProperties(
new SheetProperties().setTitle(outputSheetTitle)
);

when(mockSheetsService.spreadsheets()).thenReturn(mockSpreadsheets);
when(mockSpreadsheets.get(anyString())).thenReturn(mockSpreadsheetsGet);
when(mockSpreadsheetsGet.execute()).thenReturn(mockSpreadSheet);
when(mockSpreadSheet.getSheets()).thenReturn(new ArrayList<Sheet>(List.of(mockSheet)));
when(mockSpreadsheets.batchUpdate(anyString(),
any(BatchUpdateSpreadsheetRequest.class))).thenReturn(mockBatchUpdate);
when(mockBatchUpdate.execute()).thenReturn(mockBatchUpdateResponse);
when(mockSpreadsheets.values()).thenReturn(mockValues);
when(mockValues.append(any(), any(), any())).thenReturn(mockAppend);
when(mockAppend.setValueInputOption(anyString())).thenReturn(mockAppend);
when(mockAppend.execute()).thenReturn(mockAppendValuesResponse);
sheetsOutputStream = new SheetsOutputStream(applicationName, spreadsheetId, outputSheetTitle,
syncTime) {
@Override
Sheets createSheetsService() {
return mockSheetsService;
}
};
}

@Test
public void testConstructorWithDefaultSyncTime() throws IOException {
SheetsOutputStream stream = new SheetsOutputStream(applicationName, spreadsheetId,
outputSheetTitle) {
@Override
Sheets createSheetsService() {
return mockSheetsService;
}
};
assertNotNull(stream);
}

@Test
public void testConstructorWithCustomSyncTime() {
assertNotNull(sheetsOutputStream);
assertEquals(syncTime, sheetsOutputStream.syncTime);
}


@Test
public void testWriteSingleCharacter() throws IOException {
sheetsOutputStream.startStream();
sheetsOutputStream.write('A');
assertEquals(sheetsOutputStream.buffer.toString(), "A");
sheetsOutputStream.stopStream();

// verify stream was appended to the sheet
verify(mockValues, times(1)).append(any(), any(), any());
}


@Test
public void testWriteMultipleCharacters() throws IOException {
sheetsOutputStream.startStream();
String testString = "Hello World!";
sheetsOutputStream.write(testString.getBytes(), 0, testString.length());
assertEquals(testString, sheetsOutputStream.buffer.toString());
sheetsOutputStream.stopStream();

// verify stream was appended to the sheet
verify(mockValues, times(1)).append(any(), any(), any());
}

@Test
public void testWriteMultiLineString() throws IOException {
sheetsOutputStream.startStream();
String testString = "First line.\nSecond line.\nThird line.";
sheetsOutputStream.write(testString.getBytes(), 0, testString.length());

// verify stream was appended to the sheet
ArgumentCaptor<ValueRange> argumentCaptor = ArgumentCaptor.forClass(ValueRange.class);
verify(mockValues, times(1)).append(eq(spreadsheetId), eq(outputSheetTitle),
argumentCaptor.capture());
ValueRange capturedValue = argumentCaptor.getValue();
assertEquals(3, capturedValue.getValues().size());
assertEquals("First line.", capturedValue.getValues().get(0).get(0));
assertEquals("Second line.", capturedValue.getValues().get(1).get(0));
assertEquals("Third line.", capturedValue.getValues().get(2).get(0));

// buffer is emptied after appending to the sheet
assertEquals("", sheetsOutputStream.buffer.toString());

sheetsOutputStream.stopStream();
}

@Test
public void testAppendToSheetEmptyContent() throws IOException {
sheetsOutputStream.buffer.append(" \n \n"); // Whitespace and empty lines
sheetsOutputStream.appendToSheet();

// empty content is not appended to the sheet
verify(mockValues, never()).append(any(), any(), any());
assertEquals("", sheetsOutputStream.buffer.toString());
}

@Test
public void testAddSheetIfNotExist() throws IOException {
when(mockSpreadsheetsGet.execute()).thenReturn(
new Spreadsheet().setSheets(Collections.emptyList()));
SheetsOutputStream outputStream =
new SheetsOutputStream(applicationName, spreadsheetId, outputSheetTitle, syncTime) {
@Override
Sheets createSheetsService() {
return mockSheetsService;
}
};
verify(mockBatchUpdate, times(1)).execute();
}

@Test
public void testAddSheetFails() throws IOException {
when(mockSpreadsheetsGet.execute()).thenReturn(
new Spreadsheet().setSheets(Collections.emptyList()));
when(mockBatchUpdate.execute()).thenThrow(new IOException("Failed to add sheet"));
assertThrows(
IOException.class,
() -> new SheetsOutputStream(applicationName, spreadsheetId, outputSheetTitle,
syncTime) {
@Override
Sheets createSheetsService() {
return mockSheetsService;
}
});
}


@Test
public void testSheetExists() throws IOException {
SheetProperties sheetProperties = new SheetProperties().setTitle(outputSheetTitle);
Sheet sheet = new Sheet().setProperties(sheetProperties);
when(mockSpreadsheetsGet.execute()).thenReturn(
new Spreadsheet().setSheets(Collections.singletonList(sheet)));
SheetsOutputStream outputStream =
new SheetsOutputStream(applicationName, spreadsheetId, outputSheetTitle, syncTime) {
@Override
Sheets createSheetsService() {
return mockSheetsService;
}
};
verify(mockBatchUpdate, never()).execute();
}


@Test
public void testStartAndStopStream() throws IOException {
SheetsOutputStream outputStream =
new SheetsOutputStream(applicationName, spreadsheetId, outputSheetTitle, syncTime) {
@Override
Sheets createSheetsService() {
return mockSheetsService;
}
};
PrintStream originalOut = System.out;
PrintStream originalErr = System.err;
outputStream.startStream();
assertNotEquals(originalOut, System.out);
assertNotEquals(originalErr, System.err);
String testString = "Test output";
System.out.println(testString);
outputStream.stopStream();
assertEquals(originalOut, System.out);
assertEquals(originalErr, System.err);
}

}

0 comments on commit badb23d

Please sign in to comment.