+
+
+
+
+
+
Type: string
+
Link to a spreadsheet to observe real-time output from any tool
+
+
+
+
+
+
+
+
Example:
+
"https://docs.google.com/spreadsheets/d/<spreadsheet_id>"
+
diff --git a/gencode/java/udmi/schema/SiteLinks.java b/gencode/java/udmi/schema/SiteLinks.java
index 899cdf62e9..41fc94ca6c 100644
--- a/gencode/java/udmi/schema/SiteLinks.java
+++ b/gencode/java/udmi/schema/SiteLinks.java
@@ -19,7 +19,8 @@
"docs",
"folder",
"image",
- "repo"
+ "repo",
+ "sheet"
})
public class SiteLinks {
@@ -58,6 +59,13 @@ public class SiteLinks {
@JsonProperty("repo")
@JsonPropertyDescription("Source repository where the UDMI site model is stored")
public String repo;
+ /**
+ * Link to a spreadsheet to observe real-time output from any tool
+ *
+ */
+ @JsonProperty("sheet")
+ @JsonPropertyDescription("Link to a spreadsheet to observe real-time output from any tool")
+ public String sheet;
@Override
public int hashCode() {
@@ -65,8 +73,9 @@ public int hashCode() {
result = ((result* 31)+((this.image == null)? 0 :this.image.hashCode()));
result = ((result* 31)+((this.folder == null)? 0 :this.folder.hashCode()));
result = ((result* 31)+((this.docs == null)? 0 :this.docs.hashCode()));
- result = ((result* 31)+((this.dashboard == null)? 0 :this.dashboard.hashCode()));
result = ((result* 31)+((this.repo == null)? 0 :this.repo.hashCode()));
+ result = ((result* 31)+((this.sheet == null)? 0 :this.sheet.hashCode()));
+ result = ((result* 31)+((this.dashboard == null)? 0 :this.dashboard.hashCode()));
return result;
}
@@ -79,7 +88,7 @@ public boolean equals(Object other) {
return false;
}
SiteLinks rhs = ((SiteLinks) other);
- return ((((((this.image == rhs.image)||((this.image!= null)&&this.image.equals(rhs.image)))&&((this.folder == rhs.folder)||((this.folder!= null)&&this.folder.equals(rhs.folder))))&&((this.docs == rhs.docs)||((this.docs!= null)&&this.docs.equals(rhs.docs))))&&((this.dashboard == rhs.dashboard)||((this.dashboard!= null)&&this.dashboard.equals(rhs.dashboard))))&&((this.repo == rhs.repo)||((this.repo!= null)&&this.repo.equals(rhs.repo))));
+ return (((((((this.image == rhs.image)||((this.image!= null)&&this.image.equals(rhs.image)))&&((this.folder == rhs.folder)||((this.folder!= null)&&this.folder.equals(rhs.folder))))&&((this.docs == rhs.docs)||((this.docs!= null)&&this.docs.equals(rhs.docs))))&&((this.repo == rhs.repo)||((this.repo!= null)&&this.repo.equals(rhs.repo))))&&((this.sheet == rhs.sheet)||((this.sheet!= null)&&this.sheet.equals(rhs.sheet))))&&((this.dashboard == rhs.dashboard)||((this.dashboard!= null)&&this.dashboard.equals(rhs.dashboard))));
}
}
diff --git a/gencode/python/udmi/schema/site_metadata.py b/gencode/python/udmi/schema/site_metadata.py
index ed3d248ff7..73d1c9d2db 100644
--- a/gencode/python/udmi/schema/site_metadata.py
+++ b/gencode/python/udmi/schema/site_metadata.py
@@ -55,6 +55,7 @@ def __init__(self):
self.folder = None
self.image = None
self.repo = None
+ self.sheet = None
@staticmethod
def from_dict(source):
@@ -66,6 +67,7 @@ def from_dict(source):
result.folder = source.get('folder')
result.image = source.get('image')
result.repo = source.get('repo')
+ result.sheet = source.get('sheet')
return result
@staticmethod
@@ -96,6 +98,8 @@ def to_dict(self):
result['image'] = self.image # 5
if self.repo:
result['repo'] = self.repo # 5
+ if self.sheet:
+ result['sheet'] = self.sheet # 5
return result
from .dimension import Dimension
from .dimension import Dimension
diff --git a/schema/site_metadata.json b/schema/site_metadata.json
index bbab8c27e3..9ec087a69f 100644
--- a/schema/site_metadata.json
+++ b/schema/site_metadata.json
@@ -92,6 +92,11 @@
"description": "Source repository where the UDMI site model is stored",
"type": "string",
"examples": ["https://github.com/faucetsdn/udmi_site_model", "git@github.com: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/
"]
}
}
},
diff --git a/udmis/build.gradle b/udmis/build.gradle
index ce88919556..d11a1aaadd 100644
--- a/udmis/build.gradle
+++ b/udmis/build.gradle
@@ -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'
}
diff --git a/validator/build.gradle b/validator/build.gradle
index d89cb1c159..c242f2c388 100644
--- a/validator/build.gradle
+++ b/validator/build.gradle
@@ -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'
}
diff --git a/validator/src/main/java/com/google/udmi/util/DualOutputStream.java b/validator/src/main/java/com/google/udmi/util/DualOutputStream.java
new file mode 100644
index 0000000000..8b157c6954
--- /dev/null
+++ b/validator/src/main/java/com/google/udmi/util/DualOutputStream.java
@@ -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();
+ }
+}
diff --git a/validator/src/main/java/com/google/udmi/util/SheetsOutputStream.java b/validator/src/main/java/com/google/udmi/util/SheetsOutputStream.java
new file mode 100644
index 0000000000..344bf59588
--- /dev/null
+++ b/validator/src/main/java/com/google/udmi/util/SheetsOutputStream.java
@@ -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 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> 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 []");
+ 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/validator/src/test/java/com/google/udmi/util/SheetsOutputStreamTest.java b/validator/src/test/java/com/google/udmi/util/SheetsOutputStreamTest.java
new file mode 100644
index 0000000000..6d45e0a5aa
--- /dev/null
+++ b/validator/src/test/java/com/google/udmi/util/SheetsOutputStreamTest.java
@@ -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(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 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);
+ }
+
+}
\ No newline at end of file