diff:
@@ -389,6 +389,77 @@ static const CGFloat MDCDialogMessageOpacity = (CGFloat)0.54;
return size;
}
+- (BOOL)hasTitleIcon {
+ return (0.0f < self.titleIconImageView.image.size.height);
+}
+
+- (BOOL)hasTitle {
+ return self.title.length > 0;
+}
+
+- (BOOL)hasMessage {
+ return self.message.length > 0;
+}
+
+- (CGFloat)contentInternalVerticalPadding {
+ return (([self hasTitle] || [self hasTitleIcon]) && [self hasMessage])
+ ? MDCDialogContentVerticalPadding
+ : 0.0f;
+}
+
+- (CGFloat)contentTitleIconVerticalPadding {
+ return ([self hasTitle] && [self hasTitleIcon]) ? MDCDialogTitleIconVerticalPadding : 0.0f;
+}
+
+- (CGFloat)contentAccessoryVerticalPaddingWithFittingSize:(CGSize)boundsSize {
+ CGSize accessoryViewSize = [self.accessoryView systemLayoutSizeFittingSize:boundsSize];
+ return (([self hasTitle] || [self hasTitleIcon] || [self hasMessage]) &&
+ (0.0 < accessoryViewSize.height))
+ ? MDCDialogContentVerticalPadding
+ : 0.0f;
+}
+
+- (CGSize)titleIconSize {
+ return (self.titleIconImageView != nil) ? self.titleIconImageView.image.size : CGSizeZero;
+}
+
+- (CGFloat)titleTop {
+ return MDCDialogContentInsets.top + [self contentTitleIconVerticalPadding] +
+ [self titleIconSize].height;
+}
+
+- (CGRect)titleFrameWithTitleSize:(CGSize)titleSize {
+ return CGRectMake(MDCDialogContentInsets.left, [self titleTop], titleSize.width,
+ titleSize.height);
+}
+
+- (CGRect)messageFrameWithTitleSize:(CGSize)titleSize messageSize:(CGSize)messageSize {
+ CGRect messageFrame = CGRectMake(MDCDialogContentInsets.left,
+ CGRectGetMaxY([self titleFrameWithTitleSize:titleSize]) +
+ [self contentInternalVerticalPadding],
+ messageSize.width, messageSize.height);
+ return messageFrame;
+}
+
+- (CGRect)titleIconFrameWithTitleSize:(CGSize)titleSize {
+ CGSize titleIconSize = [self titleIconSize];
+ CGRect titleFrame = [self titleFrameWithTitleSize:titleSize];
+ // match the titleIcon alignment to the title alignment
+ CGFloat titleIconLeftPadding = MDCDialogContentInsets.left;
+ if (self.titleAlignment == NSTextAlignmentCenter) {
+ titleIconLeftPadding =
+ CGRectGetMinX(titleFrame) + (CGRectGetWidth(titleFrame) - titleIconSize.width) / 2;
+ } else if (self.titleAlignment == NSTextAlignmentRight ||
+ (self.titleAlignment == NSTextAlignmentNatural &&
+ [self mdf_effectiveUserInterfaceLayoutDirection] ==
+ UIUserInterfaceLayoutDirectionRightToLeft)) {
+ titleIconLeftPadding = CGRectGetMaxX(titleFrame) - titleIconSize.width;
+ }
+ CGRect titleIconFrame = CGRectMake(titleIconLeftPadding, MDCDialogContentInsets.top,
+ titleIconSize.width, titleIconSize.height);
+ return titleIconFrame;
+}
+
// @param boundsSize should not include any internal margins or padding
- (CGSize)calculatePreferredContentSizeForBounds:(CGSize)boundsSize {
// Even if we have more room, limit our maximum width
@@ -411,40 +482,20 @@ static const CGFloat MDCDialogMessageOpacity = (CGFloat)0.54;
CGSize boundsSize = CGRectInfinite.size;
boundsSize.width = boundingWidth - MDCDialogContentInsets.left - MDCDialogContentInsets.right;
- CGSize titleIconSize = CGSizeZero;
- if (self.titleIconImageView != nil) {
- // TODO(galiak): Have title-icon size respond to dynamic type or device screen size, once this:
- // https://github.com/material-components/material-components-ios/issues/5198 is resolved.
- titleIconSize = self.titleIconImageView.image.size;
- }
- BOOL hasTitleIcon = (0.0 < titleIconSize.height);
-
CGSize titleSize = [self.titleLabel sizeThatFits:boundsSize];
- BOOL hasTitle = (0.0 < titleSize.height);
CGSize messageSize = [self.messageLabel sizeThatFits:boundsSize];
- BOOL hasMessage = (0.0 < messageSize.height);
CGSize accessoryViewSize = [self.accessoryView systemLayoutSizeFittingSize:boundsSize];
- BOOL hasAccessory = (0.0 < accessoryViewSize.height);
+ CGFloat contentAccessoryVerticalPadding =
+ [self contentAccessoryVerticalPaddingWithFittingSize:boundsSize];
- CGFloat contentWidth = MAX(MAX(titleSize.width, messageSize.width), accessoryViewSize.width);
- contentWidth += MDCDialogContentInsets.left + MDCDialogContentInsets.right;
+ CGFloat contentWidth = MAX(MAX(titleSize.width, messageSize.width), accessoryViewSize.width) +
+ MDCDialogContentInsets.left + MDCDialogContentInsets.right;
- CGFloat contentInternalVerticalPadding = 0.0;
- if ((hasTitle || hasTitleIcon) && hasMessage) {
- contentInternalVerticalPadding = MDCDialogContentVerticalPadding;
- }
- CGFloat contentTitleIconVerticalPadding = 0.0;
- if (hasTitle && hasTitleIcon) {
- contentTitleIconVerticalPadding = MDCDialogTitleIconVerticalPadding;
- }
- CGFloat contentAccessoryVerticalPadding = 0.0;
- if ((hasTitle || hasTitleIcon || hasMessage) && hasAccessory) {
- contentAccessoryVerticalPadding = MDCDialogContentVerticalPadding;
- }
- CGFloat contentHeight = titleIconSize.height + contentTitleIconVerticalPadding +
- titleSize.height + contentInternalVerticalPadding + messageSize.height +
- contentAccessoryVerticalPadding + accessoryViewSize.height;
- contentHeight += MDCDialogContentInsets.top + MDCDialogContentInsets.bottom;
+ CGFloat contentHeight = MDCDialogContentInsets.top + [self titleIconSize].height +
+ [self contentTitleIconVerticalPadding] + titleSize.height +
+ [self contentInternalVerticalPadding] + messageSize.height +
+ contentAccessoryVerticalPadding + accessoryViewSize.height +
+ MDCDialogContentInsets.bottom;
CGSize contentSize;
contentSize.width = (CGFloat)ceil(contentWidth);
@@ -516,74 +567,35 @@ static const CGFloat MDCDialogMessageOpacity = (CGFloat)0.54;
boundsSize.width = boundsSize.width - MDCDialogContentInsets.left - MDCDialogContentInsets.right;
CGSize titleSize = [self.titleLabel sizeThatFits:boundsSize];
titleSize.width = boundsSize.width;
- BOOL hasTitle = (0.0 < titleSize.height);
-
- CGSize titleIconSize = CGSizeZero;
- if (self.titleIconImageView != nil) {
- // TODO(galiak): Have title-icon size respond to dynamic type or device screen size, once this:
- // https://github.com/material-components/material-components-ios/issues/5198 is resolved.
- titleIconSize = self.titleIconImageView.image.size;
- }
- BOOL hasTitleIcon = (0.0 < titleIconSize.height);
CGSize messageSize = [self.messageLabel sizeThatFits:boundsSize];
messageSize.width = boundsSize.width;
- BOOL hasMessage = (0.0 < messageSize.height);
CGSize accessoryViewSize =
[self.accessoryView systemLayoutSizeFittingSize:boundsSize
withHorizontalFittingPriority:UILayoutPriorityRequired
verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
accessoryViewSize.width = boundsSize.width;
- BOOL hasAccessory = (0.0 < accessoryViewSize.height);
boundsSize.width = boundsSize.width + MDCDialogContentInsets.left + MDCDialogContentInsets.right;
- CGFloat contentInternalVerticalPadding = 0.0;
- if ((hasTitle || hasTitleIcon) && hasMessage) {
- contentInternalVerticalPadding = MDCDialogContentVerticalPadding;
- }
- CGFloat contentTitleIconVerticalPadding = 0.0;
- if (hasTitle && hasTitleIcon) {
- contentTitleIconVerticalPadding = MDCDialogTitleIconVerticalPadding;
- }
- CGFloat contentAccessoryVerticalPadding = 0.0;
- if ((hasTitle || hasTitleIcon || hasMessage) && hasAccessory) {
- contentAccessoryVerticalPadding = MDCDialogContentVerticalPadding;
- }
-
- CGFloat titleTop =
- MDCDialogContentInsets.top + contentTitleIconVerticalPadding + titleIconSize.height;
- CGRect titleFrame =
- CGRectMake(MDCDialogContentInsets.left, titleTop, titleSize.width, titleSize.height);
- CGRect messageFrame = CGRectMake(MDCDialogContentInsets.left,
- CGRectGetMaxY(titleFrame) + contentInternalVerticalPadding,
- messageSize.width, messageSize.height);
+ CGFloat contentAccessoryVerticalPadding =
+ [self contentAccessoryVerticalPaddingWithFittingSize:boundsSize];
+ CGRect titleFrame = [self titleFrameWithTitleSize:titleSize];
+ CGRect messageFrame = [self messageFrameWithTitleSize:titleSize messageSize:messageSize];
CGRect accessoryViewFrame = CGRectMake(
MDCDialogContentInsets.left, CGRectGetMaxY(messageFrame) + contentAccessoryVerticalPadding,
accessoryViewSize.width, accessoryViewSize.height);
+ if (self.titleIconImageView != nil) {
+ // Match the title icon alignment to the title alignment.
+ self.titleIconImageView.frame = [self titleIconFrameWithTitleSize:titleSize];
+ }
+
self.titleLabel.frame = titleFrame;
self.messageLabel.frame = messageFrame;
self.accessoryView.frame = accessoryViewFrame;
- if (self.titleIconImageView != nil) {
- // Match the titleIcon alignment to the title alignment.
- CGFloat titleIconPosition = titleFrame.origin.x;
- if (self.titleAlignment == NSTextAlignmentCenter) {
- titleIconPosition =
- CGRectGetMinX(titleFrame) + (CGRectGetWidth(titleFrame) - titleIconSize.width) / 2;
- } else if (self.titleAlignment == NSTextAlignmentRight ||
- (self.titleAlignment == NSTextAlignmentNatural &&
- [self mdf_effectiveUserInterfaceLayoutDirection] ==
- UIUserInterfaceLayoutDirectionRightToLeft)) {
- titleIconPosition = CGRectGetMaxX(titleFrame) - titleIconSize.width;
- }
- CGRect titleIconFrame = CGRectMake(titleIconPosition, MDCDialogContentInsets.top,
- titleIconSize.width, titleIconSize.height);
- self.titleIconImageView.frame = titleIconFrame;
- }
-
// Actions
CGSize actionSize = [self calculateActionsSizeThatFitsWidth:boundsSize.width];

original_message:
Refactor out the layout calculations for title, title icon, message and the view insets into private methods.

This allows to avoid code duplication and enables easier update of the layout to GM2 design.

PiperOrigin-RevId: 292035417


message: Add iberezan to the codeowners.

change_type: MODIFY

old_path: CODEOWNERS

new_path: CODEOWNERS

diff:
/components/CollectionCells/ @yarneo
/components/CollectionLayoutAttributes/ @jverkoey
/components/Collections/ @jverkoey
-/components/Dialogs/ @galiak11
+/components/Dialogs/ @galiak11 @iberezan
/components/FeatureHighlight/ @codeman7
/components/FlexibleHeader/ @jverkoey
/components/HeaderStackView/ @jverkoey

change_type: ADD

new_path: components/Dialogs/OWNERS

diff:
+# go/mdc-contributing
+
+# Please see the main OWNERS file under the parent folder
+# material_components_ios/ to find the core owners of this component.
+approve-only: iberezan # Rollbacks and fast follows

license: Apache License 2.0

original_message:
Add iberezan to the codeowners.

PiperOrigin-RevId: 292358601


diff:
#import "MDCAlertController.h"
-#import <MDFInternationalization/MDFInternationalization.h>
-
+#import "MaterialButtons.h"
#import "MDCAlertControllerView.h"
#import "MDCDialogPresentationController.h"
#import "MDCDialogTransitionController.h"
-#import "MaterialButtons.h"
-#import "MaterialMath.h"
-#import "MaterialTypography.h"
#import "UIViewController+MaterialDialogs.h"
+#import "MaterialTypography.h"
+#import "MaterialMath.h"
+#import <MDFInternationalization/MDFInternationalization.h>
+
#import "private/MDCAlertActionManager.h"
+#import "private/MDCAlertController+Custom.h"
#import "private/MDCAlertControllerView+Private.h"
#import "private/MaterialDialogsStrings.h"
#import "private/MaterialDialogsStrings_table.h"
@@ -78,6 +79,7 @@ static NSString *const kMaterialDialogsBundle = @"MaterialDialogs.bundle";
@property(nonatomic, nullable, weak) MDCAlertControllerView *alertView;
@property(nonatomic, strong) MDCDialogTransitionController *transitionController;
@property(nonatomic, nonnull, strong) MDCAlertActionManager *actionManager;
+@property(nonatomic, nullable, strong) UIView *titleIconView;
- (nonnull instancetype)initWithTitle:(nullable NSString *)title
message:(nullable NSString *)message;
@@ -97,6 +99,7 @@ static NSString *const kMaterialDialogsBundle = @"MaterialDialogs.bundle";
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
@synthesize adjustsFontForContentSizeCategory = _adjustsFontForContentSizeCategory;
+@synthesize titleIconView = _titleIconView;
+ (instancetype)alertControllerWithTitle:(nullable NSString *)alertTitle
message:(nullable NSString *)message {
@@ -333,6 +336,13 @@ static NSString *const kMaterialDialogsBundle = @"MaterialDialogs.bundle";
}
}
+- (void)setTitleIconView:(UIView *)titleIconView {
+ _titleIconView = titleIconView;
+ if (self.alertView) {
+ self.alertView.titleIconView = titleIconView;
+ }
+}
+
- (void)setTitleIconTintColor:(UIColor *)titleIconTintColor {
_titleIconTintColor = titleIconTintColor;
if (self.alertView) {
@@ -516,6 +526,7 @@ static NSString *const kMaterialDialogsBundle = @"MaterialDialogs.bundle";
self.alertView.titleAlignment = self.titleAlignment;
self.alertView.titleIcon = self.titleIcon;
self.alertView.titleIconTintColor = self.titleIconTintColor;
+ self.alertView.titleIconView = self.titleIconView;
self.alertView.cornerRadius = self.cornerRadius;
self.alertView.enableRippleBehavior = self.enableRippleBehavior;

diff:
@property(nonatomic, strong, nullable) UIImage *titleIcon;
@property(nonatomic, strong, nullable) UIColor *titleIconTintColor;
+/**
+ An optional custom icon view above the title of the alert.
+
+ @note This property is intended to be used to provide a custom implementation of the title icon
+ view. If the intention is to just display a `UIImage`, use `setTitleIcon:` API instead. If
+ 'titleIcon' is set, 'titleIconView' is ignored.
+ */
+@property(nonatomic, strong, nullable) UIView *titleIconView;
+
@property(nonatomic, strong, nullable) UIFont *messageFont UI_APPEARANCE_SELECTOR;
@property(nonatomic, strong, nullable) UIColor *messageColor UI_APPEARANCE_SELECTOR;

diff:
+// Copyright 2018-present the Material Components for iOS authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "MaterialDialogs.h"
+
+@interface MDCAlertController (Custom)
+
+/**
+ An optional custom icon view above the title of the alert.
+
+ @note This property is intended to be used to provide a custom implementation of the title icon
+ view. If the intention is to just display a `UIImage`, use `setTitleIcon:` API instead. If
+ 'titleIcon' is set, 'titleIconView' is ignored.
+ */
+@property(nonatomic, strong, nullable) UIView *titleIconView;
+
+@end

diff:
@property(nonatomic, nonnull, strong) UILabel *messageLabel;
@property(nonatomic, nullable, strong) UIView *accessoryView;
-@property(nonatomic, nullable, strong) UIImageView *titleIconImageView;
-
@property(nonatomic, nullable, weak) MDCAlertActionManager *actionManager;
/** The scroll view that holds the @c titleLabel. */

diff:
@@ -44,6 +44,7 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
@interface MDCAlertControllerView ()
@property(nonatomic, getter=isVerticalActionsLayout) BOOL verticalActionsLayout;
+@property(nonatomic, nullable, strong) UIImageView *titleIconImageView;
@end
@@ -205,7 +206,16 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
if (titleIcon == nil) {
[self.titleIconImageView removeFromSuperview];
self.titleIconImageView = nil;
- } else if (self.titleIconImageView == nil) {
+ [self setNeedsLayout];
+ return;
+ }
+
+ if (self.titleIconView != nil) {
+ [self.titleIconView removeFromSuperview];
+ self.titleIconView = nil;
+ }
+
+ if (self.titleIconImageView == nil) {
self.titleIconImageView = [[UIImageView alloc] initWithImage:titleIcon];
self.titleIconImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.titleScrollView addSubview:self.titleIconImageView];
@@ -222,6 +232,25 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
self.titleIconImageView.tintColor = titleIconTintColor;
}
+- (void)setTitleIconView:(UIView *)titleIconView {
+ if (self.titleIconImageView != nil) {
+ NSLog(@"Warning: unintended use of the API. The following APIs are not expected to be used"
+ "together: 'setTitleIconView:' and `setTitleIcon:` API. Please set either, but not "
+ "both at the same time. If 'titleIcon' is set, 'titleIconView' is ignored.");
+ return;
+ }
+ if (_titleIconView == nil || ![_titleIconView isEqual:titleIconView]) {
+ if (_titleIconView != nil) {
+ [_titleIconView removeFromSuperview];
+ }
+ _titleIconView = titleIconView;
+ if (_titleIconView != nil) {
+ [self.titleScrollView addSubview:_titleIconView];
+ }
+ [self.titleScrollView setNeedsLayout];
+ }
+}
+
- (NSString *)message {
return self.messageLabel.text;
}
@@ -423,13 +452,19 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
: 0.0f;
}
-- (CGSize)titleIconSize {
- return (self.titleIconImageView != nil) ? self.titleIconImageView.image.size : CGSizeZero;
+- (CGSize)titleIconViewSize {
+ CGSize titleIconViewSize = CGSizeZero;
+ if (self.titleIconImageView != nil) {
+ titleIconViewSize = self.titleIconImageView.image.size;
+ } else if (self.titleIconView != nil) {
+ titleIconViewSize = self.titleIconView.frame.size;
+ }
+ return titleIconViewSize;
}
- (CGFloat)titleTop {
return MDCDialogContentInsets.top + [self contentTitleIconVerticalPadding] +
- [self titleIconSize].height;
+ [self titleIconViewSize].height;
}
- (CGRect)titleFrameWithTitleSize:(CGSize)titleSize {
@@ -444,21 +479,21 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
}
- (CGRect)titleIconFrameWithTitleSize:(CGSize)titleSize {
- CGSize titleIconSize = [self titleIconSize];
+ CGSize titleIconViewSize = [self titleIconViewSize];
CGRect titleFrame = [self titleFrameWithTitleSize:titleSize];
// match the titleIcon alignment to the title alignment
CGFloat titleIconLeftPadding = MDCDialogContentInsets.left;
if (self.titleAlignment == NSTextAlignmentCenter) {
titleIconLeftPadding =
- CGRectGetMinX(titleFrame) + (CGRectGetWidth(titleFrame) - titleIconSize.width) / 2.0f;
+ CGRectGetMinX(titleFrame) + (CGRectGetWidth(titleFrame) - titleIconViewSize.width) / 2.0f;
} else if (self.titleAlignment == NSTextAlignmentRight ||
(self.titleAlignment == NSTextAlignmentNatural &&
[self mdf_effectiveUserInterfaceLayoutDirection] ==
UIUserInterfaceLayoutDirectionRightToLeft)) {
- titleIconLeftPadding = CGRectGetMaxX(titleFrame) - titleIconSize.width;
+ titleIconLeftPadding = CGRectGetMaxX(titleFrame) - titleIconViewSize.width;
}
CGRect titleIconFrame = CGRectMake(titleIconLeftPadding, MDCDialogContentInsets.top,
- titleIconSize.width, titleIconSize.height);
+ titleIconViewSize.width, titleIconViewSize.height);
return titleIconFrame;
}
@@ -515,7 +550,7 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
CGFloat contentWidth = MAX(MAX(titleSize.width, messageSize.width), accessoryViewSize.width) +
MDCDialogContentInsets.left + MDCDialogContentInsets.right;
- CGFloat contentHeight = MDCDialogContentInsets.top + [self titleIconSize].height +
+ CGFloat contentHeight = MDCDialogContentInsets.top + [self titleIconViewSize].height +
[self contentTitleIconVerticalPadding] + titleSize.height +
[self contentInternalVerticalPadding];
@@ -620,9 +655,12 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
MDCDialogContentInsets.left, CGRectGetMaxY(messageFrame) + contentAccessoryVerticalPadding,
accessoryViewSize.width, accessoryViewSize.height);
+ CGRect titleIconImageViewRect = [self titleIconFrameWithTitleSize:titleSize];
if (self.titleIconImageView != nil) {
// Match the title icon alignment to the title alignment.
- self.titleIconImageView.frame = [self titleIconFrameWithTitleSize:titleSize];
+ self.titleIconImageView.frame = titleIconImageViewRect;
+ } else if (self.titleIconView != nil) {
+ self.titleIconView.frame = titleIconImageViewRect;
}
self.titleLabel.frame = titleFrame;

diff:
#import <XCTest/XCTest.h>
+@interface MDCAlertControllerView (Testing)
+@property(nonatomic, nullable, strong) UIImageView *titleIconImageView;
+@end
+
@interface MDCAlertControllerCustomizationTests : XCTestCase
@property(nonatomic, nullable, strong) MDCAlertController *alert;
@property(nonatomic, nullable, weak) MDCAlertControllerView *alertView;

original_message:
Add a private API for passing a custom title icon view. It can be used for passing a custom animation into the title.

PiperOrigin-RevId: 292824168


diff:
/**
An optional custom icon view above the title of the alert.
- @note This property is intended to be used to provide a custom implementation of the title icon
- view. If the intention is to just display a `UIImage`, use `setTitleIcon:` API instead. If
- 'titleIcon' is set, 'titleIconView' is ignored.
+ @discussion This property is intended to be used to provide a custom implementation of the title
+ icon view. If the intention is to just display a `UIImage`, use `setTitleIcon:` API instead. If
+ this property value is nil, the title icon is displayed above the title view. If you set this
+ property to a custom view, it is displayed instead of the title icon. Custom title views are
+ aligned with the title and may be resized to fit.
*/
@property(nonatomic, strong, nullable) UIView *titleIconView;

diff:
@@ -212,11 +212,6 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
return;
}
- if (self.titleIconView != nil) {
- [self.titleIconView removeFromSuperview];
- self.titleIconView = nil;
- }
-
if (self.titleIconImageView == nil) {
self.titleIconImageView = [[UIImageView alloc] initWithImage:titleIcon];
self.titleIconImageView.contentMode = UIViewContentModeScaleAspectFit;
@@ -235,11 +230,13 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
}
- (void)setTitleIconView:(UIView *)titleIconView {
- if (self.titleIconImageView != nil) {
+ if (titleIconView != nil && self.titleIconImageView != nil) {
NSLog(@"Warning: unintended use of the API. The following APIs are not expected to be used"
"together: 'setTitleIconView:' and `setTitleIcon:` API. Please set either, but not "
- "both at the same time. If 'titleIcon' is set, 'titleIconView' is ignored.");
- return;
+ "both at the same time. If 'titleIconView' is set, 'titleIcon' is ignored.");
+ [self.titleIconImageView removeFromSuperview];
+ self.titleIconImageView = nil;
+ [self setNeedsLayout];
}
if (_titleIconView == nil || ![_titleIconView isEqual:titleIconView]) {
if (_titleIconView != nil) {
@@ -249,7 +246,7 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
if (_titleIconView != nil) {
[self.titleScrollView addSubview:_titleIconView];
}
- [self.titleScrollView setNeedsLayout];
+ [self setNeedsLayout];
}
}
@@ -473,10 +470,10 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
- (CGSize)titleIconViewSize {
CGSize titleIconViewSize = CGSizeZero;
- if (self.titleIconImageView != nil) {
- titleIconViewSize = self.titleIconImageView.image.size;
- } else if (self.titleIconView != nil) {
+ if (self.titleIconView != nil) {
titleIconViewSize = self.titleIconView.frame.size;
+ } else if (self.titleIconImageView != nil) {
+ titleIconViewSize = self.titleIconImageView.image.size;
}
return titleIconViewSize;
}
@@ -662,11 +659,11 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
accessoryViewSize.width, accessoryViewSize.height);
CGRect titleIconImageViewRect = [self titleIconFrameWithTitleSize:titleSize];
- if (self.titleIconImageView != nil) {
+ if (self.titleIconView != nil) {
+ self.titleIconView.frame = titleIconImageViewRect;
+ } else if (self.titleIconImageView != nil) {
// Match the title icon alignment to the title alignment.
self.titleIconImageView.frame = titleIconImageViewRect;
- } else if (self.titleIconView != nil) {
- self.titleIconView.frame = titleIconImageViewRect;
}
self.titleLabel.frame = titleFrame;

original_message:
Custom title icon public API: Flip the priority between title icon and title custom view to mirror UIKit convention.

Issue: b/148428512.
PiperOrigin-RevId: 301518273


message: Add a minimum dialog width.

diff:
@@ -27,6 +27,7 @@ static const MDCFontTextStyle kMessageTextStyle = MDCFontTextStyleBody1;
static const MDCFontTextStyle kButtonTextStyle = MDCFontTextStyleButton;
static const CGFloat MDCDialogMaximumWidth = 560.0f;
+static const CGFloat MDCDialogMinimumWidth = 280.0f;
static const CGFloat MDCDialogActionButtonMinimumHeight = 36.0f;
static const CGFloat MDCDialogActionButtonMinimumWidth = 48.0f;
@@ -547,7 +548,7 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
// @param boundsSize should not include any internal margins or padding
- (CGSize)calculatePreferredContentSizeForBounds:(CGSize)boundsSize {
// Even if we have more room, limit our maximum width
- boundsSize.width = MIN(boundsSize.width, MDCDialogMaximumWidth);
+ boundsSize.width = MIN(MAX(boundsSize.width, MDCDialogMinimumWidth), MDCDialogMaximumWidth);
// Title, Content & Actions
CGSize titleViewSize = [self calculateTitleViewSizeThatFitsWidth:boundsSize.width];
@@ -556,7 +557,8 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
// Final Sizing
CGSize totalSize;
- totalSize.width = MAX(titleViewSize.width, MAX(contentSize.width, actionSize.width));
+ totalSize.width = MAX(MAX(titleViewSize.width, MAX(contentSize.width, actionSize.width)),
+ MDCDialogMinimumWidth);
totalSize.height = titleViewSize.height + contentSize.height + actionSize.height;
return totalSize;

diff:
#import "MaterialContainerScheme.h"
static NSString *const kTitleShortLatin = @"Title";
+static NSString *const kTitleLongLatin =
+ @"It's a long long long long long long long long long long long long long long long long long "
+ "long long long title";
static NSString *const kMessageShortLatin = @"A short message.";
static NSString *const kMessageLongLatin =
@"Lorem ipsum dolor sit amet, consul docendi indoctum id quo, ad unum suavitate incorrupte "
"sea. An his meis consul cotidieque, eam recteque mnesarchum et, mundi volumus cu cum. Quo "
"falli dicunt an. Praesent molestiae vim ut.";
+static NSString *const kMessageVeryLongLatin =
+ @"Lorem ipsum dolor sit amet, consul docendi indoctum id quo, ad unum suavitate incorrupte "
+ "sea. An his meis consul cotidieque, eam recteque mnesarchum et, mundi volumus cu cum. Quo "
+ "falli dicunt an. Praesent molestiae vim ut. Lorem ipsum dolor sit amet, consul docendi "
+ "indoctum id quo, ad unum suavitate incorrupte sea. An his meis consul cotidieque, eam "
+ "recteque mnesarchum et, mundi volumus cu cum. Quo falli dicunt an. Praesent molestiae vim "
+ "ut. Lorem ipsum dolor sit amet, consul docendi indoctum id quo, ad unum suavitate incorrupte "
+ "sea. An his meis consul cotidieque, eam recteque mnesarchum et, mundi volumus cu cum. Quo "
+ "falli dicunt an. Praesent molestiae vim ut. Lorem ipsum dolor sit amet, consul docendi "
+ "indoctum id quo, ad unum suavitate incorrupte sea. An his meis consul cotidieque, eam "
+ "recteque mnesarchum et, mundi volumus cu cum. Quo falli dicunt an. Praesent molestiae vim "
+ "ut. Lorem ipsum dolor sit amet, consul docendi indoctum id quo, ad unum suavitate incorrupte "
+ "sea. An his meis consul cotidieque, eam recteque mnesarchum et, mundi volumus cu cum. Quo "
+ "falli dicunt an. Praesent molestiae vim ut. Lorem ipsum dolor sit amet, consul docendi "
+ "indoctum id quo, ad unum suavitate incorrupte sea. An his meis consul cotidieque, eam "
+ "recteque mnesarchum et, mundi volumus cu cum. Quo falli dicunt an. Praesent molestiae vim "
+ "ut.";
+
@interface MDCAlertControllerConfigurationsTests : MDCSnapshotTestCase
@property(nonatomic, strong) MDCAlertController *alertController;
@property(nonatomic, strong) MDCContainerScheme *containerScheme2019;
@@ -935,4 +956,37 @@ static NSString *const kMessageLongLatin =
[self generateHighlightedSnapshotAndVerifyForAlert:self.alertController];
}
+// Min title
+- (void)testMinSizeDialog {
+ // Given
+ [self addOutlinedActionWithTitle:@"Cancel"];
+ self.alertController.title = @"A";
+
+ // When
+ [self.alertController applyThemeWithScheme:self.containerScheme2019];
+
+ // Then
+ self.alertController.view.bounds = CGRectMake(0.f, 0.f, 100.f, 100.f);
+ [self.alertController.view layoutIfNeeded];
+ [self sizeAlertToFitContent];
+ [self generateSizedSnapshotAndVerifyForView:self.alertController.view];
+}
+
+// Max size message
+- (void)testMaxSizeDialog {
+ // Given
+ [self addOutlinedActionWithTitle:@"Cancel"];
+ self.alertController.title = kTitleLongLatin;
+ self.alertController.message = kMessageVeryLongLatin;
+
+ // When
+ [self.alertController applyThemeWithScheme:self.containerScheme2019];
+
+ // Then
+ self.alertController.view.bounds = CGRectMake(0.f, 0.f, 1000.f, 1000.f);
+ [self.alertController.view layoutIfNeeded];
+ [self sizeAlertToFitContent];
+ [self generateSizedSnapshotAndVerifyForView:self.alertController.view];
+}
+
@end

original_message:
Add a minimum dialog width.

PiperOrigin-RevId: 304494482


diff:
/** The color applied to the title of Alert Controller.*/
@property(nonatomic, strong, nullable) UIColor *titleColor;
-/** The alignment applied to the title of the Alert. Default to NSTextAlignmentNatural. */
+/** The alignment applied to the title of the Alert. Defaults to NSTextAlignmentNatural. */
@property(nonatomic, assign) NSTextAlignment titleAlignment;
/** An optional icon appearing above the title of the Alert Controller.*/
/** The color applied to the message of Alert Controller.*/
@property(nonatomic, strong, nullable) UIColor *messageColor;
-/** The alignment applied to the message of Alert Controller. Default to NSTextAlignmentNatural. */
+/**
+ The alignment applied to the message of Alert Controller. Defaults to NSTextAlignmentNatural.
+ */
@property(nonatomic, assign) NSTextAlignment messageAlignment;
/**
behind the Alert (the scrim) when presented by MDCDialogPresentationController.*/
@property(nonatomic, strong, nullable) UIColor *scrimColor;
-/** The Alert's background color.*/
+/** The Alert background color.*/
@property(nonatomic, strong, nullable) UIColor *backgroundColor;
-/** The corner radius applied to the Alert Controller view. Default to 0 (no round corners) */
+/** The corner radius applied to the Alert Controller view. Defaults to 0 (no round corners) */
@property(nonatomic, assign) CGFloat cornerRadius;
-/** The elevation that will be applied to the Alert Controller view. Default to 24. */
+/** The elevation that will be applied to the Alert Controller view. Defaults to 24. */
@property(nonatomic, assign) MDCShadowElevation elevation;
/**
This property is modeled after the adjustsFontForContentSizeCategory property in the
UIContentSizeCategoryAdjusting protocol added by Apple in iOS 10.0.
- Default value is NO.
+ Defaults to NO.
*/
@property(nonatomic, readwrite, setter=mdc_setAdjustsFontForContentSizeCategory:)
BOOL mdc_adjustsFontForContentSizeCategory;
If @c NO, the font size will only be adjusted if a scaled font has been provided.
- Default value is @c YES.
+ Defaults to @c YES.
*/
@property(nonatomic, assign) BOOL adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable;
@@ -354,7 +356,7 @@ typedef NS_ENUM(NSInteger, MDCContentHorizontalAlignment) {
presented vertically (for instance, in the portrait orientation on smaller devices). When set to @c
NO, the higher emphasis actions will be displayed on the bottom.
- Default value is @c NO.
+ Defaults to @c NO.
*/
@property(nonatomic, assign) BOOL orderVerticalActionsByEmphasis;

original_message:
Fixing a repeated typo in comments "Default to" => "Defaults to".

PiperOrigin-RevId: 304618132


message: Add missing in several places of MDCAlertController.

diff:
/** The color applied to the title of Alert Controller.*/
@property(nonatomic, strong, nullable) UIColor *titleColor;
-/** The alignment applied to the title of the Alert. Defaults to NSTextAlignmentNatural. */
+/** The alignment applied to the title of the Alert. Defaults to @c NSTextAlignmentNatural. */
@property(nonatomic, assign) NSTextAlignment titleAlignment;
/** An optional icon appearing above the title of the Alert Controller.*/
@property(nonatomic, strong, nullable) UIColor *messageColor;
/**
- The alignment applied to the message of Alert Controller. Defaults to NSTextAlignmentNatural.
+ The alignment applied to the message of Alert Controller. Defaults to @c NSTextAlignmentNatural.
*/
@property(nonatomic, assign) NSTextAlignment messageAlignment;
@property(nonatomic, strong, nullable) UIColor *buttonInkColor;
/** The semi-transparent color which is applied to the overlay covering the content
- behind the Alert (the scrim) when presented by MDCDialogPresentationController.*/
+ behind the Alert (the scrim) when presented by @c MDCDialogPresentationController.*/
@property(nonatomic, strong, nullable) UIColor *scrimColor;
/** The Alert background color.*/
This property is modeled after the adjustsFontForContentSizeCategory property in the
UIContentSizeCategoryAdjusting protocol added by Apple in iOS 10.0.
- Defaults to NO.
+ Defaults to @c NO.
*/
@property(nonatomic, readwrite, setter=mdc_setAdjustsFontForContentSizeCategory:)
BOOL mdc_adjustsFontForContentSizeCategory;
of our migration to Ripple. Learn more at
https://github.com/material-components/material-components-ios/tree/develop/components/Ink#migration-guide-ink-to-ripple
- Defaults to NO.
+ Defaults to @c NO.
*/
@property(nonatomic, assign) BOOL enableRippleBehavior;
/**
- A block that is invoked when the MDCAlertController receives a call to @c
+ A block that is invoked when the @c MDCAlertController receives a call to @c
traitCollectionDidChange:. The block is called after the call to the superclass.
*/
@property(nonatomic, copy, nullable) void (^traitCollectionDidChangeBlock)
*/
@property(nonatomic, assign) BOOL adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable;
-/** MDCAlertController handles its own transitioning delegate. */
+/** @c MDCAlertController handles its own transitioning delegate. */
- (void)setTransitioningDelegate:
(_Nullable id<UIViewControllerTransitioningDelegate>)transitioningDelegate NS_UNAVAILABLE;
-/** MDCAlertController.modalPresentationStyle is always UIModalPresentationCustom. */
+/** @c MDCAlertController.modalPresentationStyle is always @c UIModalPresentationCustom. */
- (void)setModalPresentationStyle:(UIModalPresentationStyle)modalPresentationStyle NS_UNAVAILABLE;
#pragma mark - Alert Actions
Material spec recommends alerts should not have more than two actions.
- @param action Will be added to the end of MDCAlertController.actions.
+ @param action Will be added to the end of @c MDCAlertController actions.
*/
- (void)addAction:(nonnull MDCAlertAction *)action;
@@ -379,7 +379,7 @@ typedef NS_ENUM(NSInteger, MDCActionEmphasis) {
typedef void (^MDCActionHandler)(MDCAlertAction *_Nonnull action);
/**
- MDCAlertAction is passed to an MDCAlertController to add a button to the alert dialog.
+ @c MDCAlertAction is passed to an @c MDCAlertController to add a button to the alert dialog.
*/
@interface MDCAlertAction : NSObject <NSCopying, UIAccessibilityIdentification>
@@ -389,7 +389,7 @@ typedef void (^MDCActionHandler)(MDCAlertAction *_Nonnull action);
@param title The title of the button shown on the alert dialog.
@param handler A block to execute when the user selects the action.
- @return An initialized MDCActionAlert object.
+ @return An initialized @c MDCActionAlert object.
*/
+ (nonnull instancetype)actionWithTitle:(nonnull NSString *)title
handler:(__nullable MDCActionHandler)handler;
@@ -402,7 +402,7 @@ typedef void (^MDCActionHandler)(MDCAlertAction *_Nonnull action);
Unthemed actions will render all emphases as text. Apply themers to the alert
to achieve different appearance for different emphases.
@param handler A block to execute when the user selects the action.
- @return An initialized MDCActionAlert object.
+ @return An initialized @c MDCActionAlert object.
*/
+ (nonnull instancetype)actionWithTitle:(nonnull NSString *)title
emphasis:(MDCActionEmphasis)emphasis
@@ -417,7 +417,7 @@ typedef void (^MDCActionHandler)(MDCAlertAction *_Nonnull action);
@property(nonatomic, nullable, readonly) NSString *title;
/**
- The MDCActionEmphasis emphasis of the button that will be rendered for the action.
+ The @c MDCActionEmphasis emphasis of the button that will be rendered for the action.
*/
@property(nonatomic, readonly) MDCActionEmphasis emphasis;

original_message:
Add missing @c in several places of MDCAlertController.

PiperOrigin-RevId: 304629191


diff:
@@ -889,8 +889,8 @@ static const CGFloat MDCDialogMessageOpacity = 0.54f;
CGPoint buttonCenter;
CGPoint buttonOrigin;
buttonOrigin.y = self.orderVerticalActionsByEmphasis
- ? self.actionsInsets.bottom
- : self.actionsScrollView.contentSize.height - self.actionsInsets.bottom;
+ ? self.actionsInsets.top
+ : self.actionsScrollView.contentSize.height - self.actionsInsets.top;
if (self.actionsHorizontalAlignmentInVerticalLayout == MDCContentHorizontalAlignmentCenter ||
self.actionsHorizontalAlignmentInVerticalLayout == MDCContentHorizontalAlignmentJustified) {
buttonCenter.x = self.actionsScrollView.contentSize.width / 2.0f;

original_message:
Fix mixed top and bottom insets for the actions.

PiperOrigin-RevId: 314852407


message: Align flag encoding in messages

diff:
@@ -88,9 +88,7 @@ const dataBlock = {
exports.data = {
preencode (state, d) {
c.uint.preencode(state, d.fork)
-
- state.end++
-
+ state.end++ // flags
if (d.block) dataBlock.preencode(state, d.block)
if (d.seek) dataSeek.preencode(state, d.seek)
if (d.upgrade) dataUpgrade.preencode(state, d.upgrade)
@@ -99,32 +97,31 @@ exports.data = {
c.uint.encode(state, d.fork)
const s = state.start++
- let bits = 0
+ let flags = 0
if (d.block) {
- bits |= 1
+ flags |= 1
dataBlock.encode(state, d.block)
}
if (d.seek) {
- bits |= 2
+ flags |= 2
dataSeek.encode(state, d.seek)
}
if (d.upgrade) {
- bits |= 4
+ flags |= 4
dataUpgrade.encode(state, d.upgrade)
}
- state.buffer[s] = bits
+ state.buffer[s] = flags
},
decode (state) {
const fork = c.uint.decode(state)
- const bits = c.uint.decode(state)
-
+ const flags = c.uint.decode(state)
return {
fork,
- block: (bits & 1) === 0 ? null : dataBlock.decode(state),
- seek: (bits & 2) === 0 ? null : dataSeek.decode(state),
- upgrade: (bits & 4) === 0 ? null : dataUpgrade.decode(state)
+ block: (flags & 1) === 0 ? null : dataBlock.decode(state),
+ seek: (flags & 2) === 0 ? null : dataSeek.decode(state),
+ upgrade: (flags & 4) === 0 ? null : dataUpgrade.decode(state)
}
}
}
@@ -183,9 +180,7 @@ const requestUpgrade = {
exports.request = {
preencode (state, r) {
c.uint.preencode(state, r.fork)
-
- state.end++
-
+ state.end++ // flags
if (r.block) requestBlock.preencode(state, r.block)
if (r.seek) requestSeek.preencode(state, r.seek)
if (r.upgrade) requestUpgrade.preencode(state, r.upgrade)
@@ -194,32 +189,31 @@ exports.request = {
c.uint.encode(state, r.fork)
const s = state.start++
- let bits = 0
+ let flags = 0
if (r.block) {
- bits |= 1
+ flags |= 1
requestBlock.encode(state, r.block)
}
if (r.seek) {
- bits |= 2
+ flags |= 2
requestSeek.encode(state, r.seek)
}
if (r.upgrade) {
- bits |= 4
+ flags |= 4
requestUpgrade.encode(state, r.upgrade)
}
- state.buffer[s] = bits
+ state.buffer[s] = flags
},
decode (state) {
const fork = c.uint.decode(state)
- const bits = c.uint.decode(state)
-
+ const flags = c.uint.decode(state)
return {
fork,
- block: (bits & 1) === 0 ? null : requestBlock.decode(state),
- seek: (bits & 2) === 0 ? null : requestSeek.decode(state),
- upgrade: (bits & 4) === 0 ? null : requestUpgrade.decode(state)
+ block: (flags & 1) === 0 ? null : requestBlock.decode(state),
+ seek: (flags & 2) === 0 ? null : requestSeek.decode(state),
+ upgrade: (flags & 4) === 0 ? null : requestUpgrade.decode(state)
}
}
}
@@ -411,11 +405,27 @@ exports.oplogEntry = {
if (m.bitfield) bitfieldUpdate.preencode(state, m.bitfield)
},
encode (state, m) {
- state.buffer[state.start++] = (m.userData ? 1 : 0) | (m.treeNodes ? 2 : 0) | (m.treeUpgrade ? 4 : 0) | (m.bitfield ? 8 : 0)
- if (m.userData) keyValue.encode(state, m.userData)
- if (m.treeNodes) nodeArray.encode(state, m.treeNodes)
- if (m.treeUpgrade) treeUpgrade.encode(state, m.treeUpgrade)
- if (m.bitfield) bitfieldUpdate.encode(state, m.bitfield)
+ const s = state.start++
+ let flags = 0
+
+ if (m.userData) {
+ flags |= 1
+ keyValue.encode(state, m.userData)
+ }
+ if (m.treeNodes) {
+ flags |= 2
+ nodeArray.encode(state, m.treeNodes)
+ }
+ if (m.treeUpgrade) {
+ flags |= 4
+ treeUpgrade.encode(state, m.treeUpgrade)
+ }
+ if (m.bitfield) {
+ flags |= 8
+ bitfieldUpdate.encode(state, m.bitfield)
+ }
+
+ state.buffer[s] = flags
},
decode (state) {
const flags = c.uint.decode(state)

license: MIT License

repo: hypercore-protocol/hypercore

original_message: Align flag encoding in messages (#40)


diff:
@@ -56,19 +56,18 @@ test('close multiple', async function (t) {
const core = await create()
await core.append('hello world')
- const expected = ['close event', 'close 1', 'close 2', 'close 3']
+ const ev = t.test('events')
- core.on('close', () => done('close event'))
- core.close().then(() => done('close 1'))
- core.close().then(() => done('close 2'))
- core.close().then(() => done('close 3'))
+ ev.plan(4)
- await core.close()
- t.is(expected.length, 0, 'all event passed')
+ let i = 0
- function done (event) {
- t.is(event, expected.shift())
- }
+ core.on('close', () => ev.is(i++, 0, 'on close'))
+ core.close().then(() => ev.is(i++, 1, 'first close'))
+ core.close().then(() => ev.is(i++, 2, 'second close'))
+ core.close().then(() => ev.is(i++, 3, 'third close'))
+
+ await ev
})
test('storage options', async function (t) {

original_message: Use inverted flow for testing multiple `core.close()` calls (#45)


message: Add additional encryption tests

diff:
const test = require('brittle')
+const RAM = require('random-access-memory')
+const Hypercore = require('..')
const { create, replicate } = require('./helpers')
const encryptionKey = Buffer.alloc(32, 'hello world')
@@ -14,10 +16,10 @@ test('encrypted append and get', async function (t) {
t.is(a.core.tree.byteLength, 5 + a.padding)
const unencrypted = await a.get(0)
- const encrypted = await a.core.blocks.get(0)
-
t.alike(unencrypted, Buffer.from('hello'))
- t.unlike(unencrypted, encrypted)
+
+ const encrypted = await a.core.blocks.get(0)
+ t.absent(encrypted.includes('hello'))
})
test('encrypted seek', async function (t) {
@@ -74,12 +76,35 @@ test('encrypted replication', async function (t) {
})
})
-test('encrypted sessions', async function (t) {
+test('encrypted session', async function (t) {
const a = await create({ encryptionKey })
await a.append(['hello'])
- const session = a.session()
+ const s = a.session()
+
+ t.alike(a.encryptionKey, s.encryptionKey)
+ t.alike(await s.get(0), Buffer.from('hello'))
+
+ await s.append(['world'])
+
+ const unencrypted = await s.get(1)
+ t.alike(unencrypted, Buffer.from('world'))
+ t.alike(await a.get(1), unencrypted)
- t.alike(await session.get(0), Buffer.from('hello'))
+ const encrypted = await s.core.blocks.get(1)
+ t.absent(encrypted.includes('world'))
+ t.alike(await a.core.blocks.get(1), encrypted)
+})
+
+test('encrypted session before ready core', async function (t) {
+ const a = new Hypercore(RAM, { encryptionKey })
+ const s = a.session()
+
+ await a.ready()
+
+ t.alike(a.encryptionKey, s.encryptionKey)
+
+ await a.append(['hello'])
+ t.alike(await s.get(0), Buffer.from('hello'))
})

original_message: Add additional encryption tests (#55)


diff:
@@ -322,14 +322,10 @@ module.exports = class Hypercore extends EventEmitter {
}
if (value) {
- if (this.encryption) {
- this.encryption.decrypt(bitfield.start, value)
- }
-
- value = value.subarray(this.padding)
+ const byteLength = value.byteLength - this.padding
for (let i = 0; i < this.sessions.length; i++) {
- this.sessions[i].emit('download', bitfield.start, value, from)
+ this.sessions[i].emit('download', bitfield.start, byteLength, from)
}
}
}
@@ -394,13 +390,12 @@ module.exports = class Hypercore extends EventEmitter {
if (this.core.bitfield.get(index)) {
block = await this.core.blocks.get(index)
- if (this.encryption) this.encryption.decrypt(index, block)
} else {
if (opts && opts.onwait) opts.onwait(index)
- // Note that the _oncoreupdate handler decrypts inplace so we should not decrypt here
block = await this.replicator.requestBlock(index)
}
+ if (this.encryption) this.encryption.decrypt(index, block)
return this._decode(encoding, block)
}

diff:
@@ -43,14 +43,8 @@ test('encrypted replication', async function (t) {
await a.append(['a', 'b', 'c', 'd', 'e'])
t.test('with encryption key', async function (t) {
- t.plan(10)
-
const b = await create(a.key, { encryptionKey })
- b.on('download', (i, block) => {
- t.alike(block, Buffer.from([i + /* a */ 0x61]))
- })
-
replicate(a, b, t)
const r = b.download({ start: 0, end: a.length })

original_message: Only emit block length in `download` event (#58)


message: Declare `events` as dependency

original_message: Declare `events` as dependency (#61)


diff:
const hypercoreCrypto = require('hypercore-crypto')
+const b4a = require('b4a')
const Oplog = require('./oplog')
const Mutex = require('./mutex')
const MerkleTree = require('./merkle-tree')
@@ -90,7 +91,7 @@ module.exports = class Core {
await oplog.flush(header)
}
- if (opts.keyPair && !header.signer.publicKey.equals(opts.keyPair.publicKey)) {
+ if (opts.keyPair && !b4a.equals(header.signer.publicKey, opts.keyPair.publicKey)) {
throw new Error('Another hypercore is stored here')
}
@@ -176,7 +177,7 @@ module.exports = class Core {
for (const u of this.header.userData) {
if (u.key !== key) continue
- if (value && u.value.equals(value)) return
+ if (value && b4a.equals(u.value, value)) return
empty = false
break
}

diff:
@@ -215,7 +215,7 @@ class ReorgBatch extends MerkleTreeBatch {
const nodes = []
const root = verifyBlock(proof, this.tree.crypto, nodes)
- if (root === null || !root.hash.equals(this.diff.hash)) return false
+ if (root === null || !b4a.equals(root.hash, this.diff.hash)) return false
this.nodes.push(...nodes)
return this._update(nodes)
@@ -233,7 +233,7 @@ class ReorgBatch extends MerkleTreeBatch {
if (!left) break
const existing = await this.tree.get(left.index, false)
- if (!existing || !existing.hash.equals(left.hash)) {
+ if (!existing || !b4a.equals(existing.hash, left.hash)) {
diff = left
} else {
diff = n.get(ite.sibling())
@@ -355,7 +355,7 @@ module.exports = class MerkleTree {
}
addNode (node) {
- if (node.size === 0 && node.hash.equals(BLANK_HASH)) node = blankNode(node.index)
+ if (node.size === 0 && b4a.equals(node.hash, BLANK_HASH)) node = blankNode(node.index)
this.unflushed.set(node.index, node)
}
@@ -383,7 +383,7 @@ module.exports = class MerkleTree {
const indexes = flat.fullRoots(2 * length)
const roots = new Array(indexes.length)
- for (var i = 0; i < indexes.length; i++) {
+ for (let i = 0; i < indexes.length; i++) {
roots[i] = this.get(indexes[i], true)
}
@@ -545,7 +545,7 @@ module.exports = class MerkleTree {
for (const root of batch.roots) {
const existing = await this.get(root.index, false)
- if (existing && existing.hash.equals(root.hash)) continue
+ if (existing && b4a.equals(existing.hash, root.hash)) continue
batch._updateDiffRoot(root)
break
}
@@ -573,7 +573,7 @@ module.exports = class MerkleTree {
if (unverified) {
const verified = await this.get(unverified.index)
- if (!verified.hash.equals(unverified.hash)) {
+ if (!b4a.equals(verified.hash, unverified.hash)) {
throw new Error('Invalid checksum at node ' + unverified.index)
}
}
@@ -718,7 +718,7 @@ module.exports = class MerkleTree {
await new Promise((resolve, reject) => {
storage.read(0, OLD_TREE.length, (err, buf) => {
if (err) return resolve()
- if (buf.equals(OLD_TREE)) return reject(new Error('Storage contains an incompatible merkle tree'))
+ if (b4a.equals(buf, OLD_TREE)) return reject(new Error('Storage contains an incompatible merkle tree'))
resolve()
})
})

original_message: Replace `Buffer.equals()` with `b4a.equals()` (#67)


message:
Fix session encryption
My previous pull request got `this` and `s` mixed up

diff:
@@ -195,10 +195,10 @@ module.exports = class Hypercore extends EventEmitter {
// Only override the block encryption if its either not already set or if
// the caller provided a different key.
if (
- !this.encryption ||
- !b4a.equals(this.encryption.key, opts.encryptionKey)
+ !s.encryption ||
+ !b4a.equals(s.encryption.key, opts.encryptionKey)
) {
- this.encryption = new BlockEncryption(opts.encryptionKey, this.key)
+ s.encryption = new BlockEncryption(opts.encryptionKey, s.key)
}
}

diff:
@@ -126,6 +126,9 @@ test('encrypted session on unencrypted core', async function (t) {
const a = await create()
const s = a.session({ encryptionKey })
+ t.alike(s.encryptionKey, encryptionKey)
+ t.unlike(s.encryptionKey, a.encryptionKey)
+
await s.append(['hello'])
const unencrypted = await s.get(0)
@@ -135,10 +138,25 @@ test('encrypted session on unencrypted core', async function (t) {
t.absent(encrypted.includes('hello'))
})
+test('encrypted session on encrypted core, same key', async function (t) {
+ const a = await create({ encryptionKey })
+ const s = a.session({ encryptionKey })
+
+ t.alike(s.encryptionKey, a.encryptionKey)
+
+ await s.append(['hello'])
+
+ const unencrypted = await s.get(0)
+ t.alike(unencrypted, Buffer.from('hello'))
+ t.alike(unencrypted, await a.get(0))
+})
+
test('encrypted session on encrypted core, different keys', async function (t) {
const a = await create({ encryptionKey: Buffer.alloc(32, 'a') })
const s = a.session({ encryptionKey: Buffer.alloc(32, 's') })
+ t.unlike(s.encryptionKey, a.encryptionKey)
+
await s.append(['hello'])
const unencrypted = await s.get(0)

original_message:
Fix session encryption (#101)

My previous pull request got `this` and `s` mixed up


message: Allow overriding cache of sessions

diff:
@@ -191,8 +191,10 @@ module.exports = class Hypercore extends EventEmitter {
s._passCapabilities(this)
- // Pass on the cache unless explicitly disabled.
- if (opts.cache !== false) s.cache = this.cache
+ // Configure the cache unless explicitly disabled.
+ if (opts.cache !== false) {
+ s.cache = opts.cache === true || !opts.cache ? this.cache : opts.cache
+ }
ensureEncryption(s, opts)

diff:
const test = require('brittle')
+const Xache = require('xache')
const { create, replicate } = require('./helpers')
test('cache', async function (t) {
@@ -11,7 +12,7 @@ test('cache', async function (t) {
t.is(await p, await q, 'blocks are identical')
})
-test('session cache', async function (t) {
+test('session cache inheritance', async function (t) {
const a = await create({ cache: true })
await a.append(['a', 'b', 'c'])
@@ -35,6 +36,20 @@ test('session cache opt-out', async function (t) {
t.not(await p, await q, 'blocks are not identical')
})
+test('session cache override', async function (t) {
+ const a = await create({ cache: true })
+ await a.append(['a', 'b', 'c'])
+
+ const s = a.session({ cache: new Xache({ maxSize: 64, maxAge: 0 }) })
+
+ const p = a.get(0)
+ const q = s.get(0)
+ const r = s.get(0)
+
+ t.not(await p, await q, 'blocks are not identical')
+ t.is(await q, await r, 'blocks are identical')
+})
+
test('clear cache on truncate', async function (t) {
const a = await create({ cache: true })
await a.append(['a', 'b', 'c'])

original_message: Allow overriding cache of sessions (#107)


message: Add `opts.onwait` to constructor

diff:
@@ -66,7 +66,8 @@ Note that `tree`, `data`, and `bitfield` are normally heavily sparse files.
valueEncoding: 'json' | 'utf-8' | 'binary', // defaults to binary
encodeBatch: batch => { ... }, // optionally apply an encoding to complete batches
keyPair: kp, // optionally pass the public key and secret key as a key pair
- encryptionKey: k // optionally pass an encryption key to enable block encryption
+ encryptionKey: k, // optionally pass an encryption key to enable block encryption
+ onwait: () => {} // hook that is called if gets are waiting for download
}
```

diff:
@@ -70,6 +70,7 @@ module.exports = class Hypercore extends EventEmitter {
this.sessions = opts._sessions || [this]
this.auth = opts.auth || null
this.autoClose = !!opts.autoClose
+ this.onwait = opts.onwait || null
this.closing = null
this.opening = this._openSession(key, storage, opts)
@@ -662,6 +663,7 @@ module.exports = class Hypercore extends EventEmitter {
} else {
if (opts && opts.wait === false) return null
if (opts && opts.onwait) opts.onwait(index)
+ else if (this.onwait) this.onwait(index)
const activeRequests = (opts && opts.activeRequests) || this.activeRequests

original_message: Add `opts.onwait` to constructor (#115)


diff:
@@ -75,10 +75,10 @@ You can also set valueEncoding to any [abstract-encoding](https://github.com/maf
valueEncodings will be applied to individually blocks, even if you append batches. If you want to control encoding at the batch-level, you can use the `encodeBatch` option, which is a function that takes a batch and returns a binary-encoded batch. If you provide a custom valueEncoding, it will not be applied prior to `encodeBatch`.
-#### `const seq = await core.append(block)`
+#### `const { length, byteLength } = await core.append(block)`
Append a block of data (or an array of blocks) to the core.
-Returns the seq the first block was stored at.
+Returns the new length and byte length of the core.
#### `const block = await core.get(index, [options])`

diff:
@@ -51,7 +51,7 @@ module.exports = class BlockStore {
return new Promise((resolve, reject) => {
this.storage.write(offset, data, (err) => {
if (err) reject(err)
- else resolve()
+ else resolve(offset + data.byteLength)
})
})
}

diff:
@@ -295,7 +295,9 @@ module.exports = class Core {
try {
if (hooks.preappend) await hooks.preappend(values)
- if (!values.length) return this.tree.length
+ if (!values.length) {
+ return { length: this.tree.length, byteLength: this.tree.byteLength }
+ }
const batch = this.tree.batch()
for (const val of values) batch.append(val)
@@ -314,7 +316,8 @@ module.exports = class Core {
}
}
- await this._appendBlocks(values)
+ const byteLength = await this._appendBlocks(values)
+
await this.oplog.append([entry], false)
this.bitfield.setRange(batch.ancestors, batch.length - batch.ancestors, true)
@@ -329,7 +332,7 @@ module.exports = class Core {
if (this._shouldFlush()) await this._flushOplog()
- return batch.ancestors
+ return { length: batch.length, byteLength }
} finally {
this._mutex.unlock()
}

diff:
@@ -6,12 +6,12 @@ test('core - append', async function (t) {
const { core } = await create()
{
- const seq = await core.append([
+ const info = await core.append([
Buffer.from('hello'),
Buffer.from('world')
])
- t.is(seq, 0)
+ t.alike(info, { length: 2, byteLength: 10 })
t.is(core.tree.length, 2)
t.is(core.tree.byteLength, 10)
t.alike([
@@ -24,11 +24,11 @@ test('core - append', async function (t) {
}
{
- const seq = await core.append([
+ const info = await core.append([
Buffer.from('hej')
])
- t.is(seq, 2)
+ t.alike(info, { length: 3, byteLength: 13 })
t.is(core.tree.length, 3)
t.is(core.tree.byteLength, 13)
t.alike([

original_message: Return `{ length, byteLength }` object from `core.append()` (#127)


message: Remove `codecs` dependency

diff:
@@ -7,7 +7,6 @@ const b4a = require('b4a')
const Xache = require('xache')
const NoiseSecretStream = require('@hyperswarm/secret-stream')
const Protomux = require('protomux')
-const codecs = require('codecs')
const Replicator = require('./lib/replicator')
const Core = require('./lib/core')
@@ -285,7 +284,7 @@ module.exports = class Hypercore extends EventEmitter {
this.writable = !!this.auth.sign
if (opts.valueEncoding) {
- this.valueEncoding = c.from(codecs(opts.valueEncoding))
+ this.valueEncoding = c.from(opts.valueEncoding)
}
if (opts.encodeBatch) {
this.encodeBatch = opts.encodeBatch
@@ -667,7 +666,7 @@ module.exports = class Hypercore extends EventEmitter {
if (this.closing !== null) throw SESSION_CLOSED()
if (this._snapshot !== null && index >= this._snapshot.compatLength) throw SNAPSHOT_NOT_AVAILABLE()
- const encoding = (opts && opts.valueEncoding && c.from(codecs(opts.valueEncoding))) || this.valueEncoding
+ const encoding = (opts && opts.valueEncoding && c.from(opts.valueEncoding)) || this.valueEncoding
let req = this.cache && this.cache.get(index)
if (!req) req = this._get(index, opts)
@@ -820,7 +819,7 @@ module.exports = class Hypercore extends EventEmitter {
if (this.extensions.has(name)) {
const ext = this.extensions.get(name)
ext.handlers = handlers
- ext.encoding = c.from(codecs(handlers.encoding) || c.buffer)
+ ext.encoding = c.from(handlers.encoding || c.buffer)
ext.session = this
return ext
}
@@ -828,7 +827,7 @@ module.exports = class Hypercore extends EventEmitter {
const ext = {
name,
handlers,
- encoding: c.from(codecs(handlers.encoding) || c.buffer),
+ encoding: c.from(handlers.encoding || c.buffer),
session: this,
send (message, peer) {
const buffer = c.encode(this.encoding, message)

diff:
const test = require('brittle')
const RAM = require('random-access-memory')
const crypto = require('hypercore-crypto')
-const codecs = require('codecs')
+const c = require('compact-encoding')
const { create } = require('./helpers')
const Hypercore = require('../')
@@ -184,7 +184,7 @@ test('sessions - close with from option', async function (t) {
test('sessions - custom valueEncoding on session', async function (t) {
const core1 = new Hypercore(RAM)
- await core1.append(codecs('json').encode({ a: 1 }))
+ await core1.append(c.encode(c.raw.json, { a: 1 }))
const core2 = core1.session({ valueEncoding: 'json' })
await core2.append({ b: 2 })

original_message: Remove `codecs` dependency (#124)


message: Document `core.clear()`

diff:
@@ -182,6 +182,9 @@ Additional options include:
}
```
+#### `await core.clear(start, [end])`
+
+Clear stored blocks between `start` and `end`, reclaiming storage when possible.
#### `await core.truncate(newLength, [forkId])`

original_message: Document `core.clear()`


diff:
@@ -203,7 +203,7 @@ Download a range of data.
You can await when the range has been fully downloaded by doing:
```js
-await range.downloaded()
+await range.done()
```
A range can have the following properties:

diff:
@@ -12,6 +12,7 @@ const Replicator = require('./lib/replicator')
const Core = require('./lib/core')
const BlockEncryption = require('./lib/block-encryption')
const Info = require('./lib/info')
+const Download = require('./lib/download')
const { ReadStream, WriteStream } = require('./lib/streams')
const { BAD_ARGUMENT, SESSION_CLOSED, SESSION_NOT_WRITABLE, SNAPSHOT_NOT_AVAILABLE } = require('./lib/errors')
@@ -628,7 +629,7 @@ module.exports = class Hypercore extends EventEmitter {
const end = this.core.tree.length
const contig = this.contiguousLength
- await this.download({ start, end, ifAvailable: true }).downloaded()
+ await this.download({ start, end, ifAvailable: true }).done()
if (!upgraded) upgraded = this.contiguousLength !== contig
}
@@ -739,21 +740,12 @@ module.exports = class Hypercore extends EventEmitter {
}
download (range) {
- const reqP = this._download(range)
+ const req = this._download(range)
// do not crash in the background...
- reqP.catch(noop)
+ req.catch(noop)
- // TODO: turn this into an actual object...
- return {
- async downloaded () {
- const req = await reqP
- return req.promise
- },
- destroy () {
- reqP.then(req => req.context && req.context.detach(req), noop)
- }
- }
+ return new Download(req)
}
async _download (range) {

diff:
+module.exports = class Download {
+ constructor (req) {
+ this.req = req
+ }
+
+ async done () {
+ return (await this.req).promise
+ }
+
+ /**
+ * Deprecated. Use `range.done()`.
+ */
+ downloaded () {
+ return this.done()
+ }
+
+ destroy () {
+ this.req.then(req => req.context && req.context.detach(req), noop)
+ }
+}
+
+function noop () {}

diff:
@@ -50,7 +50,7 @@ test('multisig hypercore', async function (t) {
replicate(a, b, t)
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(b.length, 5)
t.end()
@@ -156,7 +156,7 @@ test('multisig hypercore with instance and extension', async function (t) {
t.is(a.length, 1)
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(a.length, 1)
@@ -214,7 +214,7 @@ test('proof-of-work hypercore', async function (t) {
replicate(a, b, t)
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(b.length, 5)
})
@@ -244,7 +244,7 @@ test('core using custom sign fn', async function (t) {
replicate(a, b, t)
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(b.length, 5)
t.end()

diff:
@@ -24,7 +24,7 @@ test('clear + replication', async function (t) {
replicate(a, b, t)
await a.append(['a', 'b', 'c'])
- await b.download({ start: 0, end: 3 }).downloaded()
+ await b.download({ start: 0, end: 3 }).done()
await a.clear(1)
@@ -43,7 +43,7 @@ test('clear + replication, gossip', async function (t) {
replicate(b, c, t)
await a.append(['a', 'b', 'c'])
- await b.download({ start: 0, end: 3 }).downloaded()
+ await b.download({ start: 0, end: 3 }).done()
await c.update()
await b.clear(1)

diff:
@@ -50,7 +50,7 @@ test('encrypted replication', async function (t) {
await t.test('through direct download', async function (t) {
const r = b.download({ start: 0, length: a.length })
- await r.downloaded()
+ await r.done()
for (let i = 0; i < 5; i++) {
t.alike(await b.get(i), await a.get(i))
@@ -75,7 +75,7 @@ test('encrypted replication', async function (t) {
await t.test('through direct download', async function (t) {
const r = b.download({ start: 0, length: a.length })
- await r.downloaded()
+ await r.done()
for (let i = 0; i < 5; i++) {
t.alike(await b.get(i), await a.core.blocks.get(i))

diff:
@@ -17,7 +17,7 @@ test('basic replication', async function (t) {
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(d, 5)
})
@@ -40,7 +40,7 @@ test('basic replication from fork', async function (t) {
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(d, 5)
t.is(a.fork, b.fork)
@@ -64,7 +64,7 @@ test('eager replication from bigger fork', async function (t) {
})
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(d, a.length)
t.is(a.fork, b.fork)
@@ -102,7 +102,7 @@ test('bigger download range', async function (t) {
})
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
t.is(b.length, a.length, 'same length')
t.is(downloaded.size, a.length, 'downloaded all')
@@ -120,7 +120,7 @@ test('high latency reorg', async function (t) {
{
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
}
s[0].destroy()
@@ -134,7 +134,7 @@ test('high latency reorg', async function (t) {
{
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
}
let same = 0
@@ -385,7 +385,7 @@ test('replicate discrete range', async function (t) {
replicate(a, b, t)
const r = b.download({ blocks: [0, 2, 3] })
- await r.downloaded()
+ await r.done()
t.is(d, 3)
t.alike(await b.get(0), Buffer.from('a'))
@@ -407,7 +407,7 @@ test('replicate discrete empty range', async function (t) {
const r = b.download({ blocks: [] })
- await r.downloaded()
+ await r.done()
t.is(d, 0)
})
@@ -506,7 +506,7 @@ test.skip('can disable downloading from a peer', async function (t) {
{
const r = c.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
}
const aPeer = b.peers[0].stream.rawStream === aStream
@@ -527,7 +527,7 @@ test.skip('can disable downloading from a peer', async function (t) {
{
const r = b.download({ start: 0, end: a.length })
- await r.downloaded()
+ await r.done()
}
t.is(aUploads, 0)
@@ -545,13 +545,13 @@ test('contiguous length', async function (t) {
replicate(a, b, t)
- await b.download({ blocks: [0, 2, 4] }).downloaded()
+ await b.download({ blocks: [0, 2, 4] }).done()
t.is(b.contiguousLength, 1, 'b has 0 through 1')
- await b.download({ blocks: [1] }).downloaded()
+ await b.download({ blocks: [1] }).done()
t.is(b.contiguousLength, 3, 'b has 0 through 2')
- await b.download({ blocks: [3] }).downloaded()
+ await b.download({ blocks: [3] }).done()
t.is(b.contiguousLength, 5, 'b has all blocks')
})
@@ -571,7 +571,7 @@ test('contiguous length after fork', async function (t) {
replicate(a, b, t)
- await b.download({ start: 0, end: a.length }).downloaded()
+ await b.download({ start: 0, end: a.length }).done()
t.is(b.contiguousLength, 3, 'b has all blocks after fork')
})
@@ -652,7 +652,7 @@ test('download blocks if available', async function (t) {
b.on('download', () => d++)
const r = b.download({ blocks: [1, 3, 6], ifAvailable: true })
- await r.downloaded()
+ await r.done()
t.is(d, 2)
})
@@ -669,7 +669,7 @@ test('download range if available', async function (t) {
b.on('download', () => d++)
const r = b.download({ start: 2, end: 6, ifAvailable: true })
- await r.downloaded()
+ await r.done()
t.is(d, 3)
})
@@ -688,7 +688,7 @@ test('download blocks if available, destroy midway', async function (t) {
})
const r = b.download({ blocks: [1, 3, 6], ifAvailable: true })
- await r.downloaded()
+ await r.done()
t.pass('range resolved')
})
@@ -707,7 +707,7 @@ test('download blocks available from when only a partial set is available', asyn
await b.get(3)
const r = c.download({ start: 0, end: -1, ifAvailable: true })
- await r.downloaded()
+ await r.done()
t.ok(!(await c.has(0)))
t.ok(!(await c.has(1)))
@@ -723,7 +723,7 @@ test('download range resolves immediately if no peers', async function (t) {
// no replication
const r = b.download({ start: 0, end: 5, ifAvailable: true })
- await r.downloaded()
+ await r.done()
t.pass('range resolved')
})
@@ -767,7 +767,7 @@ test('non-sparse snapshot during partial replication', async function (t) {
await a.append(['a', 'b', 'c', 'd', 'e'])
- await b.download({ start: 0, end: 3 }).downloaded()
+ await b.download({ start: 0, end: 3 }).done()
await c.update()
const s = c.snapshot()
@@ -776,7 +776,7 @@ test('non-sparse snapshot during partial replication', async function (t) {
await s.update()
t.is(s.contiguousLength, s.length)
- await b.download({ start: 3, end: 5 }).downloaded()
+ await b.download({ start: 3, end: 5 }).done()
t.is(s.contiguousLength, s.length)
await s.update()
@@ -794,13 +794,13 @@ test('sparse replication without gossiping', async function (t) {
let s
s = replicate(a, b)
- await b.download({ start: 0, end: 3 }).downloaded()
+ await b.download({ start: 0, end: 3 }).done()
await unreplicate(s)
await a.append(['d', 'e', 'f', 'd'])
s = replicate(a, b)
- await b.download({ start: 4, end: 7 }).downloaded()
+ await b.download({ start: 4, end: 7 }).done()
await unreplicate(s)
await t.test('block', async function (t) {
@@ -818,7 +818,7 @@ test('sparse replication without gossiping', async function (t) {
s = replicate(b, c)
t.teardown(() => unreplicate(s))
- await c.download({ start: 4, end: 6 }).downloaded()
+ await c.download({ start: 4, end: 6 }).done()
t.pass('resolved')
})
@@ -828,7 +828,7 @@ test('sparse replication without gossiping', async function (t) {
s = replicate(b, c)
t.teardown(() => unreplicate(s))
- await c.download({ blocks: [4, 6] }).downloaded()
+ await c.download({ blocks: [4, 6] }).done()
t.pass('resolved')
})

diff:
@@ -43,10 +43,10 @@ test('implicit snapshot - gets are snapshotted at call time', async function (t)
t.is(await clone.get(2), 'block #2.1')
- await range1.downloaded()
+ await range1.done()
t.pass('local range finished')
- await range2.downloaded()
+ await range2.done()
t.pass('remote range finished')
})

original_message: Rename `range.downloaded()` to `range.done()` (#303)


diff:
@@ -616,7 +616,7 @@ module.exports = class Hypercore extends EventEmitter {
async info () {
if (this.opened === false) await this.opening
- return Info.from(this.core, this.padding, this._snapshot)
+ return Info.from(this)
}
async update (opts) {

diff:
module.exports = class Info {
constructor (opts = {}) {
+ this.key = opts.key
+ this.discoveryKey = opts.discoveryKey
this.length = opts.length || 0
this.contiguousLength = opts.contiguousLength || 0
this.byteLength = opts.byteLength || 0
+ this.fork = opts.fork || 0
this.padding = opts.padding || 0
}
- static async from (core, padding, snapshot) {
+ static async from (session) {
return new Info({
- key: core.key,
- discoveryKey: core.discoveryKey,
- length: snapshot
- ? snapshot.length
- : core.tree.length,
- contiguousLength: core.header.contiguousLength,
- byteLength: snapshot
- ? snapshot.byteLength
- : (core.tree.byteLength - (core.tree.length * padding)),
- fork: core.tree.fork,
- padding
+ key: session.key,
+ discoveryKey: session.discoveryKey,
+ length: session.length,
+ contiguousLength: session.contiguousLength,
+ byteLength: session.byteLength,
+ fork: session.fork,
+ padding: session.padding
})
}
}

original_message: Fix `info.key`, `info.discoveryKey`, and `info.fork` (#317)


message: Switch to `crc-universal`

diff:
const cenc = require('compact-encoding')
const b4a = require('b4a')
-const crc32 = require('crc32-universal')
+const { crc32 } = require('crc-universal')
module.exports = class Oplog {
constructor (storage, { pageSize = 4096, headerEncoding = cenc.raw, entryEncoding = cenc.raw } = {}) {

original_message: Switch to `crc-universal` (#314)


diff:
@@ -251,12 +251,13 @@ module.exports = class Bitfield {
})
}
- static open (storage) {
+ static open (storage, tree = null) {
return new Promise((resolve, reject) => {
storage.stat((err, st) => {
if (err) return resolve(new Bitfield(storage, null))
- const size = st.size - (st.size & 3)
+ let size = st.size - (st.size & 3)
if (!size) return resolve(new Bitfield(storage, null))
+ if (tree) size = Math.min(size, ceilTo(tree.length / 8, 4096))
storage.read(0, size, (err, data) => {
if (err) return reject(err)
resolve(new Bitfield(storage, data))

diff:
@@ -108,7 +108,7 @@ module.exports = class Core {
}
const tree = await MerkleTree.open(treeFile, { crypto, ...header.tree })
- const bitfield = await Bitfield.open(bitfieldFile)
+ const bitfield = await Bitfield.open(bitfieldFile, tree)
const blocks = new BlockStore(dataFile, tree)
if (overwrite) {

original_message: Don't read empty disk pages when loading bitfield (#324)


message: Make `core.has()` accept a range

diff:
@@ -118,6 +118,10 @@ Additional options include
}
```
+#### `const has = await core.has(start, [end])`
+
+Check if the core has all blocks between `start` and `end`.
+
#### `const updated = await core.update()`
Wait for the core to try and find a signed update to it's length.

diff:
@@ -666,10 +666,15 @@ module.exports = class Hypercore extends EventEmitter {
return req.promise
}
- async has (index) {
+ async has (start, end = start + 1) {
if (this.opened === false) await this.opening
- return this.core.bitfield.get(index)
+ const length = end - start
+ if (length <= 0) return false
+ if (length === 1) return this.core.bitfield.get(start)
+
+ const i = this.core.bitfield.firstUnset(start)
+ return i === -1 || i >= end
}
async get (index, opts) {

diff:
@@ -263,3 +263,37 @@ test('defaults for wait', async function (t) {
await s.close()
await core.close()
})
+
+test('has', async function (t) {
+ const core = await create()
+ await core.append(['a', 'b', 'c', 'd', 'e', 'f'])
+
+ for (let i = 0; i < core.length; i++) {
+ t.ok(await core.has(i), `has ${i}`)
+ }
+
+ await core.clear(2)
+ t.comment('2 cleared')
+
+ for (let i = 0; i < core.length; i++) {
+ if (i === 2) {
+ t.absent(await core.has(i), `does not have ${i}`)
+ } else {
+ t.ok(await core.has(i), `has ${i}`)
+ }
+ }
+})
+
+test('has range', async function (t) {
+ const core = await create()
+ await core.append(['a', 'b', 'c', 'd', 'e', 'f'])
+
+ t.ok(await core.has(0, 5), 'has 0 to 4')
+
+ await core.clear(2)
+ t.comment('2 cleared')
+
+ t.absent(await core.has(0, 5), 'does not have 0 to 4')
+ t.ok(await core.has(0, 2), 'has 0 to 1')
+ t.ok(await core.has(3, 5), 'has 3 to 4')
+})

original_message: Make `core.has()` accept a range (#330)


message: Run `test:generate`

diff:
@@ -21,6 +21,7 @@ async function runTests () {
await import('./mutex.js')
await import('./oplog.js')
await import('./preload.js')
+ await import('./remote-bitfield.js')
await import('./replicate.js')
await import('./sessions.js')
await import('./snapshots.js')

original_message: Run `test:generate` (#332)


message: Add more error constructors

diff:
@@ -32,6 +32,18 @@ module.exports = class HypercoreError extends Error {
return new HypercoreError(msg, 'INVALID_CAPABILITY', HypercoreError.INVALID_CAPABILITY)
}
+ static INVALID_CHECKSUM (msg = 'Invalid checksum') {
+ return new HypercoreError(msg, 'INVALID_CHECKSUM', HypercoreError.INVALID_CHECKSUM)
+ }
+
+ static INVALID_OPERATION (msg) {
+ return new HypercoreError(msg, 'INVALID_OPERATION', HypercoreError.INVALID_OPERATION)
+ }
+
+ static INVALID_PROOF (msg = 'Proof not verifiable') {
+ return new HypercoreError(msg, 'INVALID_PROOF', HypercoreError.INVALID_PROOF)
+ }
+
static SNAPSHOT_NOT_AVAILABLE (msg = 'Snapshot is not available') {
return new HypercoreError(msg, 'SNAPSHOT_NOT_AVAILABLE', HypercoreError.SNAPSHOT_NOT_AVAILABLE)
}
@@ -47,4 +59,12 @@ module.exports = class HypercoreError extends Error {
static SESSION_CLOSED (msg = 'Session is closed') {
return new HypercoreError(msg, 'SESSION_CLOSED', HypercoreError.SESSION_CLOSED)
}
+
+ static OPLOG_CORRUPT (msg = 'Oplog file appears corrupt or out of date') {
+ return new HypercoreError(msg, 'OPLOG_CORRUPT', HypercoreError.OPLOG_CORRUPT)
+ }
+
+ static INVALID_OPLOG_VERSION (msg = 'Invalid header version') {
+ return new HypercoreError(msg, 'INVALID_OPLOG_VERSION', HypercoreError.INVALID_OPLOG_VERSION)
+ }
}

diff:
@@ -4,6 +4,7 @@ const c = require('compact-encoding')
const Xache = require('xache')
const b4a = require('b4a')
const caps = require('./caps')
+const { INVALID_PROOF, INVALID_CHECKSUM, INVALID_OPERATION, BAD_ARGUMENT } = require('./errors')
const BLANK_HASH = b4a.alloc(32)
const OLD_TREE = b4a.from([5, 2, 87, 2, 0, 0, 40, 7, 66, 76, 65, 75, 69, 50, 98])
@@ -26,12 +27,12 @@ class NodeQueue {
}
if (this.i >= this.nodes.length) {
- throw new Error('Expected node ' + index + ', got (nil)')
+ throw INVALID_OPERATION('Expected node ' + index + ', got (nil)')
}
const node = this.nodes[this.i++]
if (node.index !== index) {
- throw new Error('Expected node ' + index + ', got node ' + node.index)
+ throw INVALID_OPERATION('Expected node ' + index + ', got node ' + node.index)
}
this.length--
@@ -109,7 +110,7 @@ class MerkleTreeBatch {
}
commit () {
- if (!this.commitable()) throw new Error('Tree was modified during batch, refusing to commit')
+ if (!this.commitable()) throw INVALID_OPERATION('Tree was modified during batch, refusing to commit')
if (this.upgraded) this._commitUpgrade()
@@ -423,7 +424,7 @@ module.exports = class MerkleTree {
if (node !== undefined) {
if (node.hash === BLANK_HASH) {
- if (error) throw new Error('Could not load node: ' + index)
+ if (error) throw INVALID_OPERATION('Could not load node: ' + index)
return Promise.resolve(null)
}
return Promise.resolve(node)
@@ -560,7 +561,7 @@ module.exports = class MerkleTree {
}
if (!verifyUpgrade(proof, unverified, batch)) {
- throw new Error('Fork proof not verifiable')
+ throw INVALID_PROOF('Fork proof not verifiable')
}
for (const root of batch.roots) {
@@ -594,7 +595,7 @@ module.exports = class MerkleTree {
if (unverified) {
const verified = await this.get(unverified.index)
if (!b4a.equals(verified.hash, unverified.hash)) {
- throw new Error('Invalid checksum at node ' + unverified.index)
+ throw INVALID_CHECKSUM('Invalid checksum at node ' + unverified.index)
}
}
@@ -613,10 +614,10 @@ module.exports = class MerkleTree {
const node = normalizeIndexed(block, hash)
if (from >= to || to > head) {
- throw new Error('Invalid upgrade')
+ throw INVALID_OPERATION('Invalid upgrade')
}
if (seek && upgrade && node !== null && node.index >= from) {
- throw new Error('Cannot both do a seek and block/hash request when upgrading')
+ throw INVALID_OPERATION('Cannot both do a seek and block/hash request when upgrading')
}
let subTree = head
@@ -713,7 +714,7 @@ module.exports = class MerkleTree {
async byteRange (index) {
const head = 2 * this.length
if (((index & 1) === 0 ? index : flat.rightSpan(index)) >= head) {
- throw new Error('Index is out of bounds')
+ throw BAD_ARGUMENT('Index is out of bounds')
}
return [await this.byteOffset(index), (await this.get(index)).size]
}
@@ -862,7 +863,7 @@ function verifyUpgrade ({ fork, upgrade }, blockRoot, batch) {
const node = extra[i++]
while (node.index !== ite.index) {
- if (ite.factor === 2) throw new Error('Unexpected node: ' + node.index)
+ if (ite.factor === 2) throw INVALID_OPERATION('Unexpected node: ' + node.index)
ite.leftChild()
}
@@ -923,14 +924,14 @@ async function seekTrustedTree (tree, root, bytes) {
async function seekUntrustedTree (tree, root, bytes) {
const offset = await tree.byteOffset(root)
- if (offset > bytes) throw new Error('Invalid seek')
+ if (offset > bytes) throw INVALID_OPERATION('Invalid seek')
if (offset === bytes) return root
bytes -= offset
const node = await tree.get(root)
- if (node.size <= bytes) throw new Error('Invalid seek')
+ if (node.size <= bytes) throw INVALID_OPERATION('Invalid seek')
return seekTrustedTree(tree, root, bytes)
}
@@ -1061,7 +1062,7 @@ function nodesToRoot (index, nodes, head) {
for (let i = 0; i < nodes; i++) {
ite.parent()
- if (ite.contains(head)) throw new Error('Nodes is out of bounds')
+ if (ite.contains(head)) throw BAD_ARGUMENT('Nodes is out of bounds')
}
return ite.index

diff:
const c = require('compact-encoding')
const b4a = require('b4a')
+const { INVALID_OPLOG_VERSION } = require('./errors')
const EMPTY = b4a.alloc(0)
@@ -637,7 +638,7 @@ oplog.header = {
const version = c.uint.decode(state)
if (version !== 0) {
- throw new Error('Invalid header version. Expected 0, got ' + version)
+ throw INVALID_OPLOG_VERSION('Invalid header version. Expected 0, got ' + version)
}
return {

diff:
const cenc = require('compact-encoding')
const b4a = require('b4a')
const { crc32 } = require('crc-universal')
+const { OPLOG_CORRUPT } = require('./errors')
module.exports = class Oplog {
constructor (storage, { pageSize = 4096, headerEncoding = cenc.raw, entryEncoding = cenc.raw } = {}) {
@@ -77,7 +78,7 @@ module.exports = class Oplog {
this._headers[1] = 0
if (buffer.byteLength >= this._entryOffset) {
- throw new Error('Oplog file appears corrupt or out of date')
+ throw OPLOG_CORRUPT()
}
return result
}

original_message: Add more error constructors (#336)


message: Auxiliary function for getting output of the kamel cli commands was added

diff:
@@ -22,6 +22,7 @@ limitations under the License.
package support
import (
+ "bytes"
"bufio"
"context"
"encoding/json"
@@ -1311,3 +1312,21 @@ func NewTestNamespace(injectKnativeBroker bool) ctrl.Object {
}
return obj
}
+
+func GetOutputString(command *cobra.Command) string {
+ var buf bytes.Buffer
+ reader, writer, err := os.Pipe()
+ if err != nil {
+ panic(err)
+ }
+
+ command.SetOut(writer)
+ command.Execute()
+
+ writer.Close()
+ defer reader.Close()
+
+ buf.ReadFrom(reader)
+
+ return buf.String()
+}

repo: apache/camel-k


message: Kamel CLI delete test was added

diff:
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+ v1 "k8s.io/api/core/v1"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ . "github.com/apache/camel-k/e2e/support"
+)
+
+func TestKamelCLIDelete(t *testing.T) {
+ WithNewTestNamespace(t, func(ns string) {
+ Expect(Kamel("install", "-n", ns).Execute()).To(Succeed())
+
+ t.Run("delete running integration", func(t *testing.T) {
+ Expect(Kamel("run", "-n", ns, "../files/yaml.yaml").Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ Expect(Kamel("delete", "yaml", "-n", ns).Execute()).To(Succeed())
+ Eventually(Integration(ns, "yaml")).Should(BeNil())
+ Eventually(IntegrationPod(ns, "yaml")).Should(BeNil())
+ })
+
+ t.Run("delete building integration", func(t *testing.T) {
+ Expect(Kamel("run", "-n", ns, "../files/yaml.yaml").Execute()).To(Succeed())
+ Expect(Kamel("delete", "yaml", "-n", ns).Execute()).To(Succeed())
+ Eventually(Integration(ns, "yaml")).Should(BeNil())
+ Eventually(IntegrationPod(ns, "yaml")).Should(BeNil())
+ })
+
+ t.Run("delete integration from csv", func(t *testing.T) {
+ Expect(Kamel("run", "github:apache/camel-k/e2e/common/files/yaml.yaml", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ Expect(Kamel("delete", "yaml", "-n", ns).Execute()).To(Succeed())
+ Eventually(Integration(ns, "yaml")).Should(BeNil())
+ Eventually(IntegrationPod(ns, "yaml")).Should(BeNil())
+ })
+
+ t.Run("delete several integrations", func(t *testing.T) {
+ Expect(Kamel("run", "../files/yaml.yaml", "-n", ns).Execute()).To(Succeed())
+ Expect(Kamel("run", "../files/Java.java", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ Expect(Kamel("delete", "yaml", "-n", ns).Execute()).To(Succeed())
+ Eventually(Integration(ns, "yaml")).Should(BeNil())
+ Eventually(IntegrationPod(ns, "yaml")).Should(BeNil())
+ Expect(Kamel("delete", "java", "-n", ns).Execute()).To(Succeed())
+ Eventually(Integration(ns, "java")).Should(BeNil())
+ Eventually(IntegrationPod(ns, "java")).Should(BeNil())
+ })
+
+ t.Run("delete all integrations", func(t *testing.T) {
+ Expect(Kamel("run", "../files/yaml.yaml", "-n", ns).Execute()).To(Succeed())
+ Expect(Kamel("run", "../files/Java.java", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
+ Eventually(Integration(ns, "yaml")).Should(BeNil())
+ Eventually(IntegrationPod(ns, "yaml")).Should(BeNil())
+ Eventually(Integration(ns, "java")).Should(BeNil())
+ Eventually(IntegrationPod(ns, "java")).Should(BeNil())
+ })
+ })
+}

original_message: Kamel CLI delete test was added


message: Kamel CLI get test was added

diff:
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+ "fmt"
+ v1 "k8s.io/api/core/v1"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ . "github.com/apache/camel-k/e2e/support"
+)
+
+func TestKamelCLIGet(t *testing.T) {
+ WithNewTestNamespace(t, func(ns string) {
+ Expect(Kamel("install", "-n", ns).Execute()).To(Succeed())
+
+ t.Run("get integration", func(t *testing.T) {
+ Expect(Kamel("run", "../files/yaml.yaml", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ // regex is used for the compatibility of tests between OC and vanilla K8
+ // kamel get may have different output depending og the platform
+ kitName := Integration(ns, "yaml")().Status.Kit
+ regex := fmt.Sprintf("^NAME\tPHASE\tKIT\n\\s*yaml\tRunning\t(%s/%s|%s)", ns, kitName, kitName)
+ Expect(GetOutputString(Kamel("get", "-n", ns))).To(MatchRegexp(regex))
+
+ Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
+ })
+
+ t.Run("get several integrations", func(t *testing.T) {
+ Expect(Kamel("run", "../files/yaml.yaml", "-n", ns).Execute()).To(Succeed())
+ Expect(Kamel("run", "../files/Java.java", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+
+ kitName1 := Integration(ns, "java")().Status.IntegrationKit.Name
+ kitName2 := Integration(ns, "yaml")().Status.IntegrationKit.Name
+ regex := fmt.Sprintf("^NAME\tPHASE\tKIT\n\\s*java\tRunning\t" +
+ "(%s/%s|%s)\n\\s*yaml\tRunning\t(%s/%s|%s)\n", ns, kitName1, kitName1, ns, kitName2, kitName2)
+ Expect(GetOutputString(Kamel("get", "-n", ns))).To(MatchRegexp(regex))
+
+ Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
+ })
+
+ t.Run("get no integrations" , func(t *testing.T) {
+ Expect(GetOutputString(Kamel("get", "-n", ns))).NotTo(ContainSubstring("Running"))
+ Expect(GetOutputString(Kamel("get", "-n", ns))).NotTo(ContainSubstring("Building Kit"))
+ })
+ })
+}

original_message: Kamel CLI get test was added


message: Auxiliary function for async getting of cobra command output as a string was added

diff:
@@ -1372,3 +1372,15 @@ func GetOutputString(command *cobra.Command) string {
return buf.String()
}
+
+func GetOutputStringAsync(cmd *cobra.Command) func() string {
+ var buffer bytes.Buffer
+ stdout := bufio.NewWriter(&buffer)
+
+ cmd.SetOut(stdout)
+ go cmd.Execute()
+
+ return func() string {
+ return buffer.String()
+ }
+}


message: Kamel CLI log test was added

diff:
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+ v1 "k8s.io/api/core/v1"
+ "strings"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ . "github.com/apache/camel-k/e2e/support"
+)
+
+func TestKamelCLILog(t *testing.T) {
+ WithNewTestNamespace(t, func(ns string) {
+ Expect(Kamel("install", "-n", ns).Execute()).To(Succeed())
+
+ t.Run("check integration log", func(t *testing.T) {
+ Expect(Kamel("run", "../files/yaml.yaml", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
+ //first line of the integration logs
+ logs := strings.Split(IntegrationLogs(ns, "yaml")(), "\n")[0]
+ podName := IntegrationPod(ns, "yaml")().Name
+
+ logsCLI := GetOutputStringAsync(Kamel("log", "yaml", "-n", ns))
+ Eventually(logsCLI).Should(ContainSubstring("Monitoring pod " + podName))
+ Eventually(logsCLI).Should(ContainSubstring(logs))
+ })
+ })
+}

original_message: Kamel CLI log test was added


message: Kamel CLI bind test was added

diff:
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package common
+
+import (
+ v1 "k8s.io/api/core/v1"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ . "github.com/apache/camel-k/e2e/support"
+)
+
+func TestKamelCLIBind(t *testing.T) {
+ WithNewTestNamespace(t, func(ns string) {
+ Expect(Kamel("install", "-n", ns).Execute()).To(Succeed())
+ Expect(CreateTimerKamelet(ns, "test-timer-source")()).To(Succeed())
+
+ t.Run("bind timer to log", func(t *testing.T) {
+ Expect(Kamel("bind", "test-timer-source", "log:info", "-p", "source.message=helloTest", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationPodPhase(ns, "test-timer-source-to-log"), TestTimeoutLong).Should(Equal(v1.PodRunning))
+
+ Eventually(IntegrationLogs(ns, "test-timer-source-to-log")).Should(ContainSubstring("Body: helloTest"))
+ Expect(Kamel("bind", "test-timer-source", "log:info", "-p", "source.message=newText", "-n", ns).Execute()).To(Succeed())
+ Eventually(IntegrationLogs(ns, "test-timer-source-to-log")).Should(ContainSubstring("Body: newText"))
+
+ Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
+ })
+
+ t.Run("unsuccessful binding, no property", func(t *testing.T) {
+ Expect(Kamel("bind", "timer-source", "log:info", "-n", ns).Execute()).NotTo(Succeed())
+ })
+ })
+}

original_message: Kamel CLI bind test was added


message: rest_test integrations to YAML

diff:
@@ -37,7 +37,7 @@ func TestRunCronExample(t *testing.T) {
Expect(Kamel("install", "-n", ns).Execute()).To(Succeed())
t.Run("cron", func(t *testing.T) {
- Expect(Kamel("run", "-n", ns, "files/cron.groovy").Execute()).To(Succeed())
+ Expect(Kamel("run", "-n", ns, "files/cron.yaml").Execute()).To(Succeed())
Eventually(IntegrationCronJob(ns, "cron"), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(IntegrationCondition(ns, "cron", camelv1.IntegrationConditionReady), TestTimeoutMedium).Should(Equal(v1.ConditionTrue))
Eventually(IntegrationLogs(ns, "cron"), TestTimeoutMedium).Should(ContainSubstring("Magicstring!"))
@@ -53,7 +53,7 @@ func TestRunCronExample(t *testing.T) {
})
t.Run("cron-timer", func(t *testing.T) {
- Expect(Kamel("run", "-n", ns, "files/cron-timer.groovy").Execute()).To(Succeed())
+ Expect(Kamel("run", "-n", ns, "files/cron-timer.yaml").Execute()).To(Succeed())
Eventually(IntegrationCronJob(ns, "cron-timer"), TestTimeoutMedium).ShouldNot(BeNil())
Eventually(IntegrationCondition(ns, "cron-timer", camelv1.IntegrationConditionReady), TestTimeoutMedium).Should(Equal(v1.ConditionTrue))
Eventually(IntegrationLogs(ns, "cron-timer"), TestTimeoutMedium).Should(ContainSubstring("Magicstring!"))
@@ -61,7 +61,7 @@ func TestRunCronExample(t *testing.T) {
})
t.Run("cron-fallback", func(t *testing.T) {
- Expect(Kamel("run", "-n", ns, "files/cron-fallback.groovy").Execute()).To(Succeed())
+ Expect(Kamel("run", "-n", ns, "files/cron-fallback.yaml").Execute()).To(Succeed())
Eventually(IntegrationPodPhase(ns, "cron-fallback"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
Eventually(IntegrationCondition(ns, "cron-fallback", camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue))
Eventually(IntegrationLogs(ns, "cron-fallback"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
@@ -69,7 +69,7 @@ func TestRunCronExample(t *testing.T) {
})
t.Run("cron-quartz", func(t *testing.T) {
- Expect(Kamel("run", "-n", ns, "files/cron-quartz.groovy").Execute()).To(Succeed())
+ Expect(Kamel("run", "-n", ns, "files/cron-quartz.yaml").Execute()).To(Succeed())
Eventually(IntegrationPodPhase(ns, "cron-quartz"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
Eventually(IntegrationCondition(ns, "cron-quartz", camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue))
Eventually(IntegrationLogs(ns, "cron-quartz"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))

diff:
+# ---------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ---------------------------------------------------------------------------
+
+- rest:
+ verb:
+ - method: get
+ uri: "/customers/{name}"
+ to: "direct:start"
+
+- from:
+ uri: "direct:start"
+ steps:
+ - log:
+ message: "get ${header.name}"
+ - set-body:
+ simple: "${header.name} Doe"

diff:
+# ---------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ---------------------------------------------------------------------------
+
+- from:
+ uri: "timer:tick"
+ steps:
+ - to:
+ uri: "http:{{serviceName}}/customers/{{name}}"
+ - log:
+ message: "${body}"

diff:
@@ -48,17 +48,17 @@ func TestRunRest(t *testing.T) {
}
Expect(Kamel("install", "-n", ns, "--trait-profile", profile).Execute()).To(Succeed())
- Expect(Kamel("run", "-n", ns, "files/RestConsumer.java").Execute()).To(Succeed())
+ Expect(Kamel("run", "-n", ns, "files/rest-consumer.yaml").Execute()).To(Succeed())
Eventually(IntegrationPodPhase(ns, "rest-consumer"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
t.Run("Service works", func(t *testing.T) {
name := "John"
service := Service(ns, "rest-consumer")
Eventually(service, TestTimeoutShort).ShouldNot(BeNil())
- Expect(Kamel("run", "-n", ns, "files/RestProducer.groovy", "-p", "serviceName=rest-consumer", "-p", "name="+name).Execute()).To(Succeed())
+ Expect(Kamel("run", "-n", ns, "files/rest-producer.yaml", "-p", "serviceName=rest-consumer", "-p", "name="+name).Execute()).To(Succeed())
Eventually(IntegrationPodPhase(ns, "rest-producer"), TestTimeoutMedium).Should(Equal(v1.PodRunning))
- Eventually(IntegrationLogs(ns, "rest-consumer"), TestTimeoutShort).Should(ContainSubstring(fmt.Sprintf("get %s", name)))
- Eventually(IntegrationLogs(ns, "rest-producer"), TestTimeoutShort).Should(ContainSubstring(fmt.Sprintf("%s Doe", name)))
+ Eventually(IntegrationLogs(ns, "rest-consumer"), TestTimeoutLong).Should(ContainSubstring(fmt.Sprintf("get %s", name)))
+ Eventually(IntegrationLogs(ns, "rest-producer"), TestTimeoutLong).Should(ContainSubstring(fmt.Sprintf("%s Doe", name)))
})
if ocp {

original_message: rest_test integrations to YAML